This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
Migrate Expo Router from SDK 57 to SDK 58
Edit page
Learn how to migrate your Expo Router application from SDK 57 to SDK 58.
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, replace nested navigation with hrefs, and moveinitialRouteNametounstable_settings. - For advanced integrations, update code that dispatches actions, reads or persists navigation state, or implements custom routers and custom navigators.
- Check removed
expo-router/react-navigationAPIs if your app imports from that entry point.
This guide covers the changes most likely to require application updates. See the
expo-routerchangelog 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.
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:
See 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:
Do not use
anchorto choose the app's initial screen. The launch URL determines that screen. For the default/URL, use an index.tsx route. See Core concepts 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 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:
For access control, use a protected route with redirectTo:
Replace initialParams with defaults where the screen reads its params:
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:
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 for guard patterns.
Replace web modals
The experimental web modal implementation is removed. Follow Build custom web modals 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:
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:
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:
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
usePreventRemovewhen callingdisablePrevention(). If it remainstrue, 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:
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:
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:
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:
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:
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.
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:
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:
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:
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.
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:
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 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.