---
modificationDate: September 15, 2026
title: Migrate Expo Router from SDK 57 to SDK 58
description: Learn how to migrate your Expo Router application from SDK 57 to SDK 58.
---

This documentation is available as Markdown for AI agents and LLMs. See the [full Markdown index](/llms.txt) or append .md to any documentation URL.

# Migrate Expo Router from SDK 57 to SDK 58

Learn how to migrate your Expo Router application from SDK 57 to SDK 58.

<AgentInstructions>

## Submitting Feedback

If you encounter errors, misleading or outdated information, report it so Expo can be improved:

Preferred command:
npx --yes submit-expo-feedback@latest --category docs --subject "/router/migrate/sdk-57-to-58/" "<actionable feedback>"

Direct HTTP fallback:
curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/router/migrate/sdk-57-to-58/","feedback":"🤖 Agent feedback for docs: <specific, actionable description> (<model>, <harness>)"}'

Only submit when you have something specific and actionable to report. Try to give the most context.

## Navigation

When answering a related or follow-up question, fetch the relevant page below as Markdown (.md) instead of guessing; use llms.txt for the full map.

You are here: Guides > Expo Router > Migration
Pages in this section:
- [React Navigation](https://docs.expo.dev/router/migrate/from-react-navigation.md)
- [Expo Webpack](https://docs.expo.dev/router/migrate/from-expo-webpack.md)
- [SDK 55 to 56](https://docs.expo.dev/router/migrate/sdk-55-to-56.md)
- [SDK 57 to 58](https://docs.expo.dev/router/migrate/sdk-57-to-58.md) (this page)
Full documentation tree: [llms.txt](https://docs.expo.dev/llms.txt)

</AgentInstructions>

SDK 58 changes how Expo Router builds navigation state and integrates with navigators. Apps that use `Stack`, `Link`, and `useRouter` may need a few changes. Apps that import from `expo-router/react-navigation`, access navigation state, or implement custom routers and navigators require closer review.

Use the following checklist to find the sections that apply to your app:

-   For common app migrations, prefer [`useRouter`](/router/migrate/sdk-57-to-58.md#prefer-userouter-in-components), replace nested navigation with [hrefs](/router/migrate/sdk-57-to-58.md#navigate-with-complete-hrefs), and move [`initialRouteName` to `unstable_settings`](/router/migrate/sdk-57-to-58.md#move-initialroutename-to-unstable_settings).
-   For advanced integrations, update code that [dispatches actions](/router/migrate/sdk-57-to-58.md#update-navigationdispatch), [reads or persists navigation state](/router/migrate/sdk-57-to-58.md#read-navigation-state), or implements [custom routers](/router/migrate/sdk-57-to-58.md#update-custom-routers) and [custom navigators](/router/migrate/sdk-57-to-58.md#update-custom-navigators).
-   Check [removed `expo-router/react-navigation` APIs](/router/migrate/sdk-57-to-58.md#review-removed-expo-routerreact-navigation-exports) if your app imports from that entry point.

> This guide covers the changes most likely to require application updates. See the [`expo-router` changelog](https://github.com/expo/expo/blob/main/packages/expo-router/CHANGELOG.md#5800--2026-09-10) for the complete list of SDK 58 changes.

## Common migrations

### Prefer `useRouter` in components

Prefer the router returned by `useRouter()` over `useNavigation().navigate()` or the module-level `router` for href-based navigation from a component. The hook is bound to the Expo Router root that rendered the component.

The module-level `router` is still available, but it throws before the first router render and cannot distinguish between multiple router roots. Continue to use `useNavigation` when you need navigator-specific APIs, such as events, options, or action dispatching.

`useRouter()` includes the common navigation methods, such as `push`, `navigate`, `replace`, `back`, `dismiss`, `dismissTo`, `dismissAll`, `canGoBack`, `canDismiss`, `setParams`, and `prefetch`. If a required navigation method is missing, [open an issue](https://github.com/expo/expo/issues).

### Navigate with complete hrefs

In SDK 57, React Navigation could interpret `screen`, `params`, and `initial` as instructions for building nested state. In SDK 58, they are ordinary user parameters. Navigate to a complete href instead:

```diff
- import { useNavigation } from 'expo-router';
+ import { useRouter } from 'expo-router';
- const navigation = useNavigation();
+ const router = useRouter();
- navigation.navigate('(tabs)', {
- screen: 'feed',
+ router.push({
+ pathname: '/(tabs)/feed/[id]',
  params: { id: '42' },
  });
```

See [Navigate to a screen in a nested navigator](/router/advanced/nesting-navigators.md#navigate-to-a-screen-in-a-nested-navigator) for more examples.

Ancestor route params are also no longer copied into descendant routes during imperative navigation. If your app relies on a descendant receiving a parameter from its parent route, include the value in the destination href. This makes imperative navigation consistent with opening the same href from a deep link or cold start.

### Move `initialRouteName` to `unstable_settings`

The `initialRouteName` navigator prop is removed. To add a back destination when deep linking into a stack, export `unstable_settings.anchor` from that stack's layout:

```diff
import { Stack } from 'expo-router';
+ export const unstable_settings = {
+ anchor: 'index',
+ };
  export default function FeedLayout() {
- return <Stack initialRouteName="index" />;
+ return <Stack />;
  }
```

> Do not use `anchor` to choose the app's initial screen. The launch URL determines that screen. For the default `/` URL, use an **index.tsx** route. See [Core concepts](/router/basics/core-concepts.md) for details.

Expo Router now builds complete initial state from the URL. For example, a stack nested in tabs reads the setting from that stack's own **_layout.tsx** file. During imperative navigation, pass `{ withAnchor: true }` when the anchor route should be inserted below the destination.

See [Router settings](/router/advanced/router-settings.md) for initial route and anchor behavior.

### Replace `redirect` and `initialParams`

Layout `Screen` components no longer accept `redirect` or `initialParams`.

Render a `Redirect` from the route file instead of configuring `redirect` on a screen:

```tsx app/legacy.tsx
import { Redirect } from 'expo-router';

export default function LegacyRoute() {
  return <Redirect href="/replacement" />;
}
```

For access control, use a protected route with `redirectTo`:

```tsx app/_layout.tsx
import { Stack } from 'expo-router';

import { useAuth } from '../context/auth';

export default function RootLayout() {
  const isSignedIn = useAuth();

  return (
    <Stack>
      <Stack.Protected guard={isSignedIn} redirectTo="/sign-in">
        <Stack.Screen name="account" />
      </Stack.Protected>
      <Stack.Screen name="sign-in" />
    </Stack>
  );
}
```

Replace `initialParams` with defaults where the screen reads its params:

```tsx app/feed.tsx
import { useLocalSearchParams } from 'expo-router';

export default function Feed() {
  const { sort = 'latest' } = useLocalSearchParams<{ sort?: string }>();
  // ...
}
```

### Declare tabs and drawer screens

JavaScript tabs, top tabs, drawer, headless tabs, and native tabs now show only screens declared in the layout. Declare every route that should appear in the navigator UI:

```tsx app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" />
      <Tabs.Screen name="feed" />
      <Tabs.Screen name="hidden" options={{ href: null }} />
    </Tabs>
  );
}
```

### Update protected routes

Most apps that use protected routes do not need to make changes. Protected routes remain registered in their navigator in SDK 58. A route whose guard fails renders a redirect instead of being removed from the route tree.

Set `redirectTo` on the navigator's `Protected` component only when you want to control the redirect destination. Without it, Expo Router uses the navigator's accessible anchor or initial route, then its first accessible route.

See [Protected routes](/router/advanced/protected.md) for guard patterns.

### Replace web modals

The experimental web modal implementation is removed. Follow [Build custom web modals](/router/advanced/web-modals.md) to render modal overlays with a custom navigator. Custom implementations can import `NativeStackView` from `expo-router` for the native stack view.

### Replace `freezeOnBlur`

The `freezeOnBlur` option has no effect in SDK 58. Remove it from your screen configuration. To preserve state while cleaning up effects after a screen loses focus, set `activityEnabled` on the navigator or a declared screen. A value of `1` hides the screen content as soon as another screen is focused:

```tsx app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      {/* Uses the stack default and hides content when two screens are above it. */}
      <Stack.Screen name="feed" activityEnabled />
      {/* Hides content as soon as another screen is focused. */}
      <Stack.Screen name="account" activityEnabled={1} />
    </Stack>
  );
}
```

When `activityEnabled` is `true`, a stack hides a screen's content once two screens sit above that screen. Tabs and drawers hide a screen's content as soon as it loses focus. Stack navigators and screens also accept a positive number to override that threshold.

To hide only part of a screen, wrap that content in `NavigationAwareActivity` instead. Its `hideWhenNestedAtLevel` prop uses the same threshold behavior and defaults to `2`.

### Install optional native dependencies

SDK 58 makes `expo-symbols` and `@expo/ui` optional peer dependencies of Expo Router. Install only the dependency required by the APIs your app uses:

```sh
# npm
# Android md icons in native tabs
npx expo install expo-symbols
# Android Stack.Toolbar
npx expo install @expo/ui

# yarn
# Android md icons in native tabs
yarn expo install expo-symbols
# Android Stack.Toolbar
yarn expo install @expo/ui

# pnpm
# Android md icons in native tabs
pnpm expo install expo-symbols
# Android Stack.Toolbar
pnpm expo install @expo/ui

# bun
# Android md icons in native tabs
bun expo install expo-symbols
# Android Stack.Toolbar
bun expo install @expo/ui
```

After installing either dependency, rebuild development builds that use the affected native API.

### Prevent a screen from being removed

Existing `usePreventRemove` calls only need changes when their callback repeats or replaces the blocked action. To continue the exact blocked action, set the prevention condition to `false` and call the callback's `repeat` function.

Use `usePreventRemove` from `expo-router` to block removal while a screen has unsaved data:

```tsx app/edit-profile.tsx
import { usePreventRemove } from 'expo-router';
import { useState } from 'react';
import { Alert, Button } from 'react-native';

export default function EditProfile() {
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  usePreventRemove(hasUnsavedChanges, ({ repeat }) => {
    Alert.alert('Discard changes?', 'Your changes have not been saved.', [
      { text: 'Keep editing', style: 'cancel' },
      {
        text: 'Discard',
        style: 'destructive',
        onPress: () => {
          setHasUnsavedChanges(false);
          repeat();
        },
      },
    ]);
  });

  return <Button title="Save" onPress={() => setHasUnsavedChanges(false)} />;
}
```

To navigate somewhere other than the blocked destination, retain the `disablePrevention` function returned by `usePreventRemove`. Set the prevention condition to `false`, call `disablePrevention()`, and then navigate. Do not dispatch from a raw `removePrevented` listener while prevention is still active because the action can be blocked again.

> Always update the boolean passed to `usePreventRemove` when calling `disablePrevention()`. If it remains `true`, the hook warns and does not re-enable prevention until the boolean changes.

## Advanced migrations

### Update `navigation.dispatch`

`navigation.dispatch` and navigation helper actions are now queued until after the current React commit. Use `navigation.dispatchSync(action)` only when an integration must apply an action immediately.

Dispatch functions are no longer supported. Read the state first, compute an action, and dispatch the action object:

```diff
- navigation.dispatch(state =>
- CommonActions.reset({
- ...state,
- index: 0,
- routes: [state.routes[0]],
- })
- );
+ const state = navigation.getState();
+ const action = CommonActions.reset({
+ ...state,
+ index: 0,
+ routes: [state.routes[0]],
+ });
+ navigation.dispatch(action);
```

The read and deferred dispatch are not atomic. Use `dispatchSync(action)` deliberately if other navigation work cannot happen between them.

### Replace `navigationKey`

Layout `Screen` and `Group` components no longer accept `navigationKey`. There is no direct replacement. Use route and layout identity, protected routes, or explicit href navigation based on why the key was used.

### Replace `beforeRemove` and `__unsafe_action__`

The `beforeRemove` and `__unsafe_action__` events are removed. Use `removed` to observe a completed removal and `removePrevented` to observe an action blocked by `usePreventRemove`.

The `removed` event fires after the route unmounts. Defer cleanup of its listener so it can receive that event:

```tsx
useEffect(() => {
  const unsubscribe = navigation.addListener('removed', event => {
    logRemoval(event.data.action);
  });

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

Do not update the removed screen's state from this listener because the screen has already unmounted.

### Move `useNavigation` inside a navigator

`useNavigation` now throws when it is called outside a navigator. If you render `ExpoRoot` manually, this includes components rendered by its `wrapper` prop. Move the hook into a route or layout rendered inside the navigation tree.

### Read navigation state

Navigation state is an implementation detail. Prefer `usePathname`, `useSegments`, and `useGlobalSearchParams` when you need URL or route information.

#### Handle optional `type` and `history`

The navigation state `type` is optional for custom routers. `history` is optional in tab and drawer state. Add fallback values before reading them:

```ts
const history = state.history ?? [];

if (state.type === 'tab') {
  // Handle tab-specific state.
}
```

#### Read stack preloaded routes from `routes`

`StackNavigationState.preloadedRoutes` is removed. Preloaded routes are appended to `state.routes` after the focused index:

```ts
const activeRoutes = state.routes.slice(0, state.index + 1);
const preloadedRoutes = state.routes.slice(state.index + 1);
```

#### Use `routeNames` for tab order

`TabNavigationState.preloadedRouteKeys` is removed. Preloaded tabs are unfocused entries in `state.routes` with `isPreloaded: true`, and lazy routes may be absent from that array. Iterate over `state.routeNames` when you need the declared tab order:

```ts
for (const routeName of state.routeNames) {
  const route = state.routes.find(route => route.name === routeName);
  // `route` is undefined when the lazy route has not been created.
}
```

#### Remove drawer `default`

`DrawerNavigationState.default` is removed. In a component, use `useDrawerStatus` from `expo-router/drawer` to read whether the drawer is open or closed. `getDrawerStatusFromState` is deprecated and now requires the router's default status as its second argument.

#### Include `routeKeySeq` in complete state

Persisted initial state must be complete. Every nested state needs a state key, route keys, `routeKeySeq`, `routeNames`, `index`, and `stale: false`. Expo Router throws when it receives incomplete persisted initial state.

Navigation state returned by a custom router extension still contains `routeKeySeq`, but the extension does not update it directly. Preserve the current state with `...state` and use the `nextKey` function provided by `extendRouter`. The wrapper writes the updated sequence into the returned state.

`CommonActions.reset` also requires state with `stale: false`, but the router generates omitted route keys and fills an omitted `routeKeySeq` from the current state.

Use `...state` to preserve the current navigator's keys, route names, and key sequence when resetting its state. See the example in [Update `navigation.dispatch`](/router/migrate/sdk-57-to-58.md#update-navigationdispatch).

Persisted state must retain valid keys and route names at every nested level.

### Update custom routers

In SDK 58, create custom routers by extending `StackRouter` or `TabRouter` with `extendRouter` or `extendRouterActions`. Expo Router builds the complete initial state, while the wrapper preserves the base router's behavior and state invariants.

#### Update the router interface

Move the parts of an existing custom router that differ from its base router into an extension. Members that the extension does not override are inherited:

| SDK 57 API | SDK 58 migration |
| --- | --- |
| `getInitialState` | Remove it. Expo Router builds initial state. |
| `getRehydratedState` | Remove it. Persisted data must provide complete state. |
| `getStateForRouteNamesChange` | Inherit the base behavior, or handle `ROUTE_NAMES_CHANGED` in the extension. |
| `routeParamList` | Remove it from `RouterConfigOptions`. Put parameter defaults in route code. |
| `RouterActionOptions` | Use `RouterConfigOptions` in `getStateForAction`. |
| Other router members | Inherit them from the base router unless the extension changes the behavior. |

The base stack and tab routers handle `PUSH`. If an extension overrides `getStateForAction`, delegate actions that it does not handle to the provided `baseRouter`.

#### Return the affected route key

`getStateForAction` now returns the next state together with the key of the route affected by the action:

```diff
- return nextState;
+ // `affectedRoute` is selected by this action's handling logic.
+ return {
+ state: nextState,
+ affectedRouteKey: affectedRoute?.key,
+ };
```

Set `affectedRouteKey` to the route selected or changed by that specific action, which is not always the focused route. Return `null` when the router cannot handle an action.

#### Extend a built-in router

`extendRouter` receives the base router and a `nextKey` function. `nextKey` generates deterministic route keys, and the wrapper writes the updated `routeKeySeq` into returned state:

```ts
import {
  attachRouteState,
  extendRouter,
  StackRouter,
  type CommonNavigationAction,
  type StackActionType,
} from 'expo-router';

type CustomAction = {
  type: 'CUSTOM-ACTION';
  payload: { name: string; params?: object };
};

const CustomStackRouter = extendRouter(StackRouter, ({ baseRouter, nextKey }) => ({
  getStateForAction(
    state,
    action: CommonNavigationAction | StackActionType | CustomAction,
    config
  ) {
    if (action.type === 'CUSTOM-ACTION') {
      if (!state.routeNames.includes(action.payload.name)) {
        return null;
      }

      const route = attachRouteState(
        {
          key: nextKey(action.payload.name),
          name: action.payload.name,
          params: action.payload.params,
        },
        action
      );
      const activeRoutes = state.routes.slice(0, state.index + 1);
      const preloadedRoutes = state.routes.slice(state.index + 1);

      return {
        state: {
          ...state,
          index: activeRoutes.length,
          routes: [...activeRoutes, route, ...preloadedRoutes],
        },
        affectedRouteKey: route.key,
      };
    }

    return baseRouter.getStateForAction(state, action, config);
  },
}));
```

Use `extendRouterActions` when only `getStateForAction` needs customization. Return `undefined` from its reducer to delegate an action. When overriding `getStateForAction` with `extendRouter`, delegate unhandled actions to `baseRouter` as shown above.

#### Make the router type optional when appropriate

`extendRouter` inherits the base router's `type`. If an extension changes the state to require a different type, pass that type in the wrapper's options. An extension whose state type is optional can omit it.

### Update custom navigators

For new custom integrations, use `createStandardRouterNavigator` in an app or `integrateWithRouter` in a reusable library. These APIs use the `standard-navigation` contract to keep navigator state, descriptors, actions, and events aligned with Expo Router.

```diff
- import { withLayoutContext } from 'expo-router';
- import { createNavigator } from './navigator';
+ import { createStandardRouterNavigator, TabRouter } from 'expo-router';
+ import { TabNavigatorContent } from './navigator';
- export const Tabs = withLayoutContext(createNavigator().Navigator);
+ export const Tabs = createStandardRouterNavigator(TabNavigatorContent, TabRouter);
```

`withLayoutContext` remains supported for existing React Navigation navigators. Remove its former third `useOnlyUserDefinedScreens` argument if your integration uses it. Filesystem routes remain registered. Use descriptor `routeSource` when navigator UI needs to distinguish layout-declared routes from filesystem routes.

The example assumes `TabNavigatorContent` has been converted from a React Navigation navigator factory to a component that accepts `NavigatorContentProps`. Library authors should instead create a framework-independent navigator with `createStandardNavigator` from `standard-navigation`, then pass that navigator to `integrateWithRouter`.

The `createStackNavigator` export is removed from `expo-router/js-stack`. This does not affect `createNativeStackNavigator` from `expo-router/native-stack`, which remains available. For new stack integrations, prefer a standard navigator with `createStandardRouterNavigator`, or pass it to `integrateWithRouter` with the matching props helper:

| Navigator | `createProps` helper |
| --- | --- |
| Custom stack | `createBaseStackProps` |
| Expo Router JavaScript stack | `createJSStackProps` |
| Expo Router native stack | `createNativeStackProps` |
| Custom tab | `createBaseTabProps` |
| Expo Router JavaScript tabs | `createJSTabsProps` |
| Expo Router JavaScript top tabs | `createJSTopTabsProps` |
| Expo Router native tabs | `createNativeTabsProps` |

Import the base and native stack helpers from `expo-router`. Import the navigator-specific helpers from the corresponding `expo-router/js-stack`, `expo-router/js-tabs`, `expo-router/js-top-tabs`, or `expo-router/native-tabs` entry point.

The `key` on `descriptor.route` can be `undefined` for a declared route that does not yet have a live state route. Update custom navigator code to handle that case.

See [Custom navigators](/router/advanced/custom-navigators.md) for standard navigator concepts.

### Review removed `expo-router/react-navigation` exports

The following compatibility APIs are removed or changed in SDK 58. Imports that remain supported are listed by the [`expo-router/react-navigation` entry point](https://github.com/expo/expo/blob/main/packages/expo-router/src/react-navigation/index.ts).

| Removed API | Replacement | Change |
| --- | --- | --- |
| `UNSTABLE_UnhandledLinkingContext` | No app-level replacement. Expo Router owns unhandled link processing. | [#49616](https://github.com/expo/expo/pull/49616) |
| `BaseNavigationContainer`, `NavigationContainer` | Let Expo Router or `ExpoRoot` own the navigation container. | [#49587](https://github.com/expo/expo/pull/49587), [#48760](https://github.com/expo/expo/pull/48760) |
| Root `options` event, `DocumentTitleOptions`, `documentTitle` | Use Expo Router `Head` or a `<title>` element for web metadata. | [#49590](https://github.com/expo/expo/pull/49590) |
| `onStateChange` on container props | Prefer Expo Router state hooks, or listen for the navigation ref's `state` event. | [#49588](https://github.com/expo/expo/pull/49588) |
| `NavigationIndependentTree`, `useNavigationIndependentTree` | Use `NavigationContainer` from `@react-navigation/native` for an isolated embedded navigation tree. | [#49172](https://github.com/expo/expo/pull/49172) |
| React Navigation `Link`, `LinkProps`, `useLinkProps` | Use `Link` and `LinkProps` from `expo-router` with an `href`. | [#48895](https://github.com/expo/expo/pull/48895) |
| `navigateDeprecated`, `navigationInChildEnabled` | Navigate to a complete href with `useRouter`. | [#49102](https://github.com/expo/expo/pull/49102) |
| `NavigatorScreenParams`, `getActionFromState`, `LinkingOptions.getActionFromState` | Use complete hrefs and let Expo Router resolve navigation state. | [#49297](https://github.com/expo/expo/pull/49297) |
| `resetRoot` on navigation container refs | Use `router.replace` or dispatch `CommonActions.reset` with complete state. | [#49297](https://github.com/expo/expo/pull/49297) |
| `beforeRemove`, `__unsafe_action__` | Use `usePreventRemove`, `removePrevented`, and `removed`. | [#49408](https://github.com/expo/expo/pull/49408) |
| `PreventRemoveContext`, `usePreventRemoveContext`, `PreventRemoveProvider` | Use `usePreventRemove`. Expo Router owns the provider. | [#49408](https://github.com/expo/expo/pull/49408), [#48347](https://github.com/expo/expo/pull/48347) |
| `Router.getInitialState`, `Router.getRehydratedState` | Let Expo Router build initial state and return complete state from custom routers. | [#48783](https://github.com/expo/expo/pull/48783), [#49297](https://github.com/expo/expo/pull/49297) |
| `Router.getStateForRouteNamesChange` | Handle `ROUTE_NAMES_CHANGED` in `getStateForAction`. | [#48479](https://github.com/expo/expo/pull/48479) |
| `RouterActionOptions`, `RouterConfigOptions.routeParamList` | Use `RouterConfigOptions` without `routeParamList`. | [#48783](https://github.com/expo/expo/pull/48783) |
| `DrawerNavigationState.default` | Use `useDrawerStatus`, or pass a default status to the deprecated `getDrawerStatusFromState`. | [#48750](https://github.com/expo/expo/pull/48750) |
| Static navigation APIs and types | Use Expo Router file routes and layouts. | [#48071](https://github.com/expo/expo/pull/48071) |
| `LinkingOptions.enabled` | Remove the option. Expo Router owns linking. | [#49103](https://github.com/expo/expo/pull/49103) |
| `UNSTABLE_routeNamesChangeBehavior` | Use protected route redirects and explicit href navigation. | [#47985](https://github.com/expo/expo/pull/47985) |
| `useOnlyUserDefinedScreens` | Remove the option. All filesystem routes remain registered. | [#47983](https://github.com/expo/expo/pull/47983) |
