Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Expo Router

Navigating between pages

Learn the different ways to link to and navigate to pages in Expo Router.

Once you have a few pages in your app and their layouts setup, it's time to start navigating between them. Every page in Expo Router has a URL by default, so you can navigate between pages using links and the same URL patterns you use on the web.

Native navigation basics with useRouter

Like in React Navigation, you can call a function from an onPress handler to navigate to another page. In Expo Router, you can use the useRouter hook to access navigation functions:

import { useRouter } from 'expo-router';
import { Button } from 'react-native';

export default function Home() {
  const router = useRouter();

  return <Button title="Go to About" onPress={() => router.navigate('/about')} />;
}

Expo Router apps default to stack navigation, where navigating to a new route pushes a screen onto a stack, and backing out of that route pops it off the stack.

  • Usually, you would want to use the router.navigate function.
  • This will either push a new page onto the stack or unwind to an existing route on the stack.
  • However, you can also call router.push to explicitly push a new page onto the stack, router.back to go back to the previous page, or router.replace to replace the current page on the stack.

With Expo Router, you refer to pages by their URL, or their position relative to the src/app directory. Check out the following file structure and how you would navigate to each page:

  • src
    • app
      • index.tsxrouter.navigate("/")
      • about.tsxrouter.navigate("/about")
      • profile
        • index.tsxrouter.navigate("/profile")
        • friends.tsxrouter.navigate("/profile/friends")

Router imperative navigation API reference — Learn how to use all the functions available for imperative navigation.

The typical way to link to a page in Expo Router is to use links like web apps. Expo Router has a Link component for navigating between pages, where the href is the same route you would use in router.navigate:

import { View } from 'react-native';
import { Link } from 'expo-router';

export default function Page() {
  return (
    <View>
      <Link href="/about">About</Link>
    </View>
  );
}

By default, the Link component renders its children inside a <Text> element.

  • This means non-text children (such as View) may have unexpected layout behavior.
  • To have full control over the layout, use the asChild prop with a Pressable or another component that accepts onPress/onClick props:
import { Pressable, Text } from 'react-native';
import { Link } from 'expo-router';

export default function Page() {
  return (
    <Link href="/other" asChild>
      <Pressable>
        <Text>Home</Text>
      </Pressable>
    </Link>
  );
}

Link API reference — Learn the options available when using Links for navigation.

Link preview — Learn how to add a preview to your link on iOS when using Expo Router.

Relative routes

You don't always have to use the absolute path to a route. Using paths that start with ./ (for the current directory) or ../ (for the parent directory) will navigate relative to the current route.

A relative URL is a URL prefix with ./, such as ./article, or ./article/. Relative URLs are resolved relative to the current rendered screen.

<Link href="./article">Go to article</Link>
router.navigate('./article');

Dynamic routes and URL parameters

Using dynamic routes with Expo Router — Learn how to make a segment of a route dynamic.

Dynamic routes can be linked to with their full URL, or by passing a params object.

Consider the following file structure:

  • src
    • app
      • user
        • [id].tsx

Each of these links will navigate to the same page:

import { Link, router } from 'expo-router';
import { View, Pressable, Text } from 'react-native';

export default function Page() {
  return (
    <View>
      <Link
        href="/user/bacon">
        View user (id inline)
      </Link>
      <Link
        href={{
          pathname: '/user/[id]',
          params: { id: 'bacon' }
        }}
      >
        View user (id in params in href)
      </Link>
      <Pressable
        onPress={() =>
          router.navigate({
            pathname: '/user/[id]',
            params: { id: 'bacon' }
          })
        }
      >
        <Text>View user (imperative)</Text>
      </Pressable>
    </View>
  );
}

Some parameters are reserved for internal use by Expo Router and React Navigation. You can find them in the Using URL parameters guide.

Passing query parameters

You can specify query parameters in the link URL itself, or as additional parameters in the params object. Any parameters that don't match the name of the dynamic route variable are equivalent to query parameters.

<Link href="/users?limit=20">View users</Link>

<Link
  href={{
    pathname: '/users',
    params: { limit: 20 }
  }}>
  View users
</Link>

Using dynamic route variables and query parameters in the destination page

All variables in the link URL are accessible to the receiving page via the useLocalSearchParams hook. This hook returns an object with all the URL parameters, including those passed as params.

For example, if you have a link like this:

<Link href="/users?limit=20">View users</Link>

Then you can read the parameters on the other end like this:

import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';

export default function Users() {
  const { id, limit } = useLocalSearchParams();

  return (
    <View>
      <Text>User ID: {id}</Text>
      <Text>Limit: {limit}</Text>
    </View>
  );
}

Updating query parameters without navigating

Query parameters can be updated without navigating to a new page. This can be done with a Link that uses the same URL as the current page, but with updated query parameters, or imperatively.

<Link href="/users?limit=50">View more users</Link>

<Pressable onPress={() => router.setParams({ limit: 50 })}>
  <Text>View more users</Text>
</Pressable>

Using URL parameters — Learn more about how to set and use URL parameters in Expo Router.

Redirects

You can immediately redirect to another route from a page or layout with the Redirect component. This functions like the replace imperative navigation function. A redirect will navigate to the new route without rendering the current page.

import { Redirect } from 'expo-router';

export default function Page() {
  return <Redirect href="/about" />;
}

Prefetching

The prefetch prop on a <Link /> component enables prefetching of the target screen when the component is rendered. This allows for faster navigation by preparing the screen in advance.

import { Link } from 'expo-router';

export default function Page() {
  return <Link href="/about" prefetch />;
}

When prefetch is set, Expo Router will attempt to render the target screen off-screen. The exact behavior depends on the type of navigator used:

  • Expo Router Navigators: Render the target screen off-screen to enable preloading.
  • Custom Navigators: May implement prefetching differently or not support it at all.

When a screen is preloaded in a stack navigator, it will have a few limitations:

  • It cannot use the imperative router API.
  • It cannot update options with useNavigation().setOptions()
  • It cannot listen to events from the navigator (for example focus, tabPress, and so on).

The navigation object will be updated once you navigate to the screen. So if you have an event listener in a useEffect hook, and have a dependency on navigation, it will add any listeners when the screen is navigated to:

const navigation = useNavigation();

useEffect(() => {
  const unsubscribe = navigation.addListener('tabPress', () => {
    // do something
  });

  return () => {
    unsubscribe();
  };
}, [navigation]);

Similarly, for dispatching actions or updating options, you can check if the screen is focused before doing so:

const navigation = useNavigation();

if (navigation.isFocused()) {
  navigation.setOptions({ title: 'Updated title' });
}

For more information, refer to the React Navigation preload docs

Deep linking is when a URL opens a specific page in your app.

  • Expo Router supports deep linking by default, so you can link to any page in your app with a URL from outside of your app, as you would inside your app with Link.
  • This is especially useful for sharing links to specific pages in your app.

Assuming your scheme is myapp, here are some examples of how you would link to a page in your app from a web page or another app:

  • src
    • app
      • about.tsxmyapp://about
      • profile
        • index.tsxmyapp://profile
      • users
        • [username].tsxmyapp://users/evanbacon

With app links and universal links, you can also link to your app with an https URL. For more information, see Universal linking.

Initial routes

When opening a deep link to a page in your app, you will likely want back navigation to work as if the user navigated to the page from your home page. To do this, you can specify an initialRouteName configuration, which defines the page in a layout that should be loaded before the deep linked page.

Consider the following file structure:

  • src
    • app
      • index.tsx
      • stack
        • index.tsx
        • second.tsx
        • _layout.tsx

stack is a stack navigator, and /stack/index is always the first route in the stack.

To ensure that /stack/index is always loaded first, even if the user deep links to /stack/second, you can set the initialRouteName in src/app/stack/_layout.tsx:

export const unstable_settings = {
  // Ensure any route can link back to `/`
  initialRouteName: 'index',
};

By default, the initialRouteName is only considered when deep linking and not during navigation within your app. However, you can use the withAnchor prop on Link to force the initial route to be loaded when navigating directly into another stack inside your app.

So, if src/app/index.tsx contained a link to /stack/second, add the withAnchor prop to ensure that /stack/index is loaded first, which will cause the user to go back to /stack/index when they press the back button from /stack/second:

<Link href="/stack/second" withAnchor>
  Go to second
</Link>

If you are missing a back button when testing deep links, this can often be fixed by setting an initialRouteName.

URL parameters

URL parameters include both route parameters and search parameters. Expo Router provides hooks for accessing and modifying these parameters.

Difference between route and search parameters

Route parameters are dynamic segments defined in a URL path, such as /profile/[user], where user is a route parameter. They are used to match a route.

Search parameters (also known as query params) are serializable fields that can be appended to a URL, such as /profile?extra=info, where extra is a search parameter. They are commonly used to pass data between pages.

Local versus global URL parameters

In apps with nested navigators, you'll often have multiple pages mounted at the same time.

  • For example, a stack has the previous page and current page in memory when a new route is pushed.

  • Because of this, Expo Router provides two different hooks for accessing URL parameters:

  • useLocalSearchParams: Returns the URL parameters for the current component. It only updates when the global URL conforms to the route.

  • useGlobalSearchParams: Returns the global URL regardless of the component. It updates on every URL param change and might cause components to update extraneously in the background.

The hooks useGlobalSearchParams and useLocalSearchParams allow you to access these parameters within your components, enabling you to retrieve and utilize both types of URL parameters.

Both hooks are typed and accessed the same way. However, the only difference is how frequently they update.

The example below demonstrates the difference between useLocalSearchParams and useGlobalSearchParams. It uses the following app directory structure:

  • src
    • app
      • _layout.tsx
      • index.tsx
      • [user].tsx

The Root Layout is a stack navigator:

import { Stack } from 'expo-router';

export default function Layout() {
  return <Stack />;
}

The initial route redirects to the dynamic route src/app/[user].tsx, with user=evanbacon:

import { Redirect } from 'expo-router';

export default function Route() {
  return <Redirect href="/evanbacon" />;
}

The dynamic route src/app/[user] prints out the global and local URL parameters (route parameters, in this case). It also allows for pushing new instances of the same route with different route parameters:

import { Text, View } from 'react-native';
import { useLocalSearchParams, useGlobalSearchParams, Link } from 'expo-router';

const friends = ['charlie', 'james']

export default function Route() {
  const glob = useGlobalSearchParams();
  const local = useLocalSearchParams();

  console.log("Local:", local.user, "Global:", glob.user);

  return (
    <View>
      <Text>User: {local.user}</Text>
      {friends.map(friend => (
        <Link key={friend} href={`/${friend}`}>
          Visit {friend}
        </Link>
      ))}
    </View>
  );
}

When the app starts, the following log is printed:

Local: evanbacon Global: evanbacon

Pressing "Visit charlie" pushes a new instance of /[user] with user=charlie, and logs the following:

# This log came from the new screen
Local: charlie Global: charlie
# This log came from the first screen
Local: evanbacon Global: charlie

Pressing "Visit james" has a similar effect:

# This log came from the new, "/james" screen
Local: james Global: james
# This log came from the "/evanbacon" screen
Local: evanbacon Global: james
# This log came from the "/charlie" screen
Local: charlie Global: james

Results:

  • useGlobalSearchParams made the background screens re-render when the URL route parameters changed. It can cause performance issues if overused.
  • Global re-renders are executed in order of the stack, so the first screen is re-rendered first, then the user=charlie screen is re-rendered after.
  • useLocalSearchParams remained the same, even when the global URL route parameters changed. You can leverage this behavior for data fetching to ensure the previous screen's data is still available when you navigate back.

Statically-typed URL parameters

Both the useLocalSearchParams and useGlobalSearchParams can be statically typed using a generic. The following is an example for the user route parameter:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Route() {
  const { user } = useLocalSearchParams<{ user: string }>();

  return <Text>User: {user}</Text>;
}

// Given the URL: `/evanbacon`
// The following is returned: { user: "evanbacon" }

Any search parameters (for example, ?query=...) can be typed optionally:

const { user, query } = useLocalSearchParams<{ user: string; query?: string }>();

// Given the URL: `/evanbacon?query=hello`
// The following is returned: { user: "evanbacon", query: "hello" }

When used with the rest syntax (...), route parameters are returned as a string array:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Route() {
  const { everything } = useLocalSearchParams<{
    everything: string[];
  }>();
  const user = everything[0];

  return <Text>User: {user}</Text>;
}

// Given the URL: `/evanbacon/123`
// The following is returned: { everything: ["evanbacon", "123"] }

Any search parameters will continue to be returned as individual strings:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Route() {
  const { everything } = useLocalSearchParams<{
    everything: string[];
    query?: string;
    query2?: string;
  }>();
  const user = everything[0];

  return <Text>User: {user}</Text>;
}

// Given the URL: `/evanbacon/123?query=hello&query2=world`
// The following is returned: { everything: ["evanbacon", "123"], query: "hello", query2: "world" }

Updating URL parameters

URL parameters can be updated using the router.setParams function from the imperative API. Updating a URL parameter will not push anything new to the history stack.

The following example uses a <TextInput> to update the search parameter q:

import { useLocalSearchParams, router } from 'expo-router';
import { useState } from 'react';
import { TextInput, View } from 'react-native';

export default function Page() {
  const params = useLocalSearchParams<{ query?: string }>();
  const [search, setSearch] = useState(params.query);

  return (
    <TextInput
      value={search}
      onChangeText={search => {
        setSearch(search);
        router.setParams({ query: search });
      }}
      placeholderTextColor="#A0A0A0"
      placeholder="Search"
      style={{
        borderRadius: 12,
        backgroundColor: '#fff',
        fontSize: 24,
        color: '#000',
        margin: 12,
        padding: 16,
      }}
    />
  );
}

Here is an example using an onPress event to update the route parameter user:

import { useLocalSearchParams, router } from 'expo-router';
import { Text } from 'react-native';

export default function User() {
  const params = useLocalSearchParams<{ user: string }>();

  return (
    <>
      <Text>User: {params.user}</Text>
      <Text onPress={() => router.setParams({ user: 'evan' })}>Go to Evan</Text>
    </>
  );
}

Route parameters versus search parameters

Route parameters are used to match a route, while search parameters are used to pass data between routes. Consider the following structure, where a route parameter is used to match the user route:

  • src
    • app
      • index.tsx
      • [user].tsxuser is a route parameter

When the src/app/[user] route is matched, the user parameter is passed to the component and never a nullish value. Both search and route parameters can be used together and are accessible with the useLocalSearchParams and useGlobalSearchParams hooks:

import { useLocalSearchParams } from 'expo-router';

export default function User() {
  const {
    // The route parameter
    user,
    // An optional search parameter.
    tab,
  } = useLocalSearchParams<{ user: string; tab?: string }>();

  console.log({ user, tab });

  // Given the URL: `/bacon?tab=projects`, the following is printed:
  // { user: 'bacon', tab: 'projects' }

  // Given the URL: `/expo`, the following is printed:
  // { user: 'expo', tab: undefined }
}

Whenever a route parameter is changed, the component will re-mount.

import { Text } from 'react-native';
import { router, useLocalSearchParams, Link } from 'expo-router';

export default function User() {
  // All three of these will change the route parameter `user`, and add a new user page.
  return (
    <>
      <Text onPress={() => router.setParams({ user: 'evan' })}>Go to Evan</Text>
      <Text onPress={() => router.push('/mark')}>Go to Mark</Text>
      <Link href="/charlie">Go to Charlie</Link>
    </>
  );
}

Hash support

The URL hash is a string that follows the # symbol in a URL.

  • It is commonly used on websites to link to a specific section of a page, but it can also be used to store data.
  • Expo Router treats the hash as a special search parameter using the name #.
  • It can be accessed and modified using the same hooks and APIs from search parameters.
import { Text } from 'react-native';
import { router, useLocalSearchParams, Link } from 'expo-router';

export default function User() {
  // Access the hash
  const { '#': hash } = useLocalSearchParams<{ '#': string }>();

  return (
    <>
      <Text onPress={() => router.setParams({ '#': 'my-hash' })}>Set a new hash</Text>
      <Text onPress={() => router.push('/#my-hash')}>Push with a new hash</Text>
      <Link href="/#my-hash">Link with a hash</Link>
    </>
  );
}

Reserved parameters

Certain URL parameters are reserved for internal use by Expo Router and React Navigation. Avoid using the following names for your own parameters to prevent conflicts:

  • screen
  • params
  • initial
  • state

Typed routes

Available when using TypeScript in your project. Expo Router supports standard TypeScript out of the box. See the TypeScript guide for more information on how to set it up.

Expo Router supports generating TypeScript types automatically with Expo CLI. This enables <Link>, and the hooks API to be statically typed. This feature is currently in beta and is not enabled by default.

Get started

Quick start

If you created your project using the Expo Router quick start guide then your project is already configured to use typed routes.

  • The Expo CLI will generate the required type file the first time you run npx expo start.
  • Then, you can use autocomplete for href props whenever you use an Expo Router <Link> component in a .tsx file.

Manual configuration

While the feature is in beta, you can enable it by setting experiments.typedRoutes to true in your app.json:

{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

Run npx expo customize tsconfig.json to configure your tsconfig.json to add the required includes fields.

Then, start the development server by running npx expo start. You can now use autocomplete in the Expo Router <Link> component's href prop.

Type generation

Typed routes in Expo Router are automatically generated when the development server starts.

  • By default, these generated types are configured to be untracked by Git and will be added to the local .gitignore file.
  • This ensures that autogenerated files do not clutter your version control system.

If you find yourself in a situation where you need to generate these types without initiating the development server, such as during type checking on a Continuous Integration (CI) server. To do this, run the command npx expo customize tsconfig.json on the CI.

Statically typed routes

Components and functions that use Href<T> will now be statically typed and have a much stricter definition. For example:

✅ <Link href="/about" />
✅ <Link href="/user/1" />
✅ <Link href={`/user/${id}`} />
✅ <Link href={("/user" + id) as Href} />
// TypeScript errors if href is not a valid route
❌ <Link href="/usser/1" />

Note: A Route type is also made available by expo-router, which matches all valid routes in a project automatically.

For dynamic routes, Href's need to be objects and their parameters are strictly typed:

✅ <Link href={{ pathname: "/user/[id]", params: { id: 1 }}} />
// TypeScript errors as href is valid, but it should be a HrefObject with params
❌ <Link href="/user/[id]" />
// TypeScript errors as params contain invalid keys
❌ <Link href={{ pathname: "/user/[id]", params: { _id: 1 }}} />
// TypeScript errors as params contain unknown keys
❌ <Link href={{ pathname: "/user/[id]", params: { id: 1, id2: 2 }}} />

Relative paths

Statically typed routes do not support relative paths. You'll need to use absolute paths for all routes:

✅ <Link href="/about" />

// Relative paths are not supported
❌ <Link href="./about" />

You can leverage the useSegments() hooks from expo-router to create complex relative paths. Consider the following structure:

  • src
    • app
      • (feed)
        • _layout.tsx
        • feed.tsx
        • search.tsx
        • profile.tsx
      • (search)
        • profile.tsx
    • components
      • button.tsx

You can ensure that you push to the same tab by using the useSegments() hook to get the first segment of the current route.

import { Link, useSegments } from 'expo-router';

export function Button() {
  const [
    // This will be either `(feed)` or `(search)` depending on the current tab.
    first,
  ] = useSegments();

  return <Link href={`/${first}/profile`}>Push profile</Link>;
}

Now, you can leverage <Button /> from both src/app/(feed)/feed.tsx and src/app/(search)/search.tsx to push ./profile while preserving the current tab.

If you require the segments for a specific use, you can pass the full route to useSegments:

import { Link, useSegments } from 'expo-router';

export function useMySegments() {
  const segments = useSegments<'/(search)/profile'>();
  //    ^? segments = ['(search)', 'profile']
  return segments;
}

Imperative navigation

You can use the typed router object to navigate imperatively:

import { router } from 'expo-router';

router.push('/about');

Or with the typed useRouter() hook:

import { useRouter } from 'expo-router';

function Page() {
  const router = useRouter();

  router.push('/about');

  // ...
}

Route parameters

For strongly typed route parameters, you can pass a full href to the useLocalSearchParams and useGlobalSearchParams hooks. For example:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Page() {
  const {
    profile, // string
    search, // string[]
  } = useLocalSearchParams<'/(search)/[profile]/[...search]'>();

  return (
    <>
      <Text>Profile: {profile}</Text>
      <Text>Search: {search.join(',')}</Text>
    </>
  );
}

Query parameters

Most query parameters will not be represented in the file system and therefore cannot be typed automatically. You can type query parameters manually by passing a generic to the useLocalSearchParams and useGlobalSearchParams hooks. For example:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Page() {
  const { query } = useLocalSearchParams<{ query?: string }>();

  return <Text>Search: {query ?? 'unset'}</Text>;
}

If you need a combination of route and query parameters, pass the route as the first generic and then the query parameters:

import { Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function Page() {
  const { query, profile, search } = useLocalSearchParams<
    '/[profile]/[...search]',
    { query?: string }
  >();

  return <Text>Search: {query ?? 'unset'}</Text>;
}

Changes made to the environment

When typed routes is enabled, Expo CLI will generate a git ignored expo-env.d.ts file in your project's root directory, update the .gitignore to ignore the new root expo-env.d.ts file, and modify the tsconfig.json to include the new expo-env.d.ts file.

The includes field in your tsconfig.json gets updated to include expo-env.d.ts and a hidden .expo directory. These entries are required and should not be removed from the file.

The generated expo-env.d.ts should not be removed or changed at any time. It should not be committed and should be ignored by version control.

Global types

Expo CLI will add the following global types to your project when typed routes is enabled:

  • Sets process.env.NODE_ENV = "development" | "production" | "test"
  • Allows the importing of .[css|sass|scss] files
  • Sets the exports of *.module.[css|sass|scss] to be Record<string, string>
  • Add types for Metro's require.context. This is enabled by expo/metro-config and used for static route generation.

Sources

The original Expo docs for this page:

On this page