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:

Common migrations

Use CSS for web safe-area spacing

Replace web spacing based on SafeAreaView or useSafeAreaInsets() with CSS. Follow the safe-area guide's web instructions for setup and SDK 58 behavior changes.

Review server rendering behavior

In SDK 58, web.output: "server" renders HTML pages on each request instead of prerendering them during export.

To keep the behavior of web.output: "server" from SDK 57 and earlier, set web.output to "static" and enable apiRoutes: true in the expo-router config plugin in your app config:

app.json
{ "expo": { "web": { "output": "static" }, "plugins": [["expo-router", { "apiRoutes": true }]] } }

This combines prerendered HTML with API routes and still requires a deployed server. To render HTML on each request, keep web.output: "server" and follow the server rendering guide.

Migrate static page metadata

In SDK 58, apps using web.output: "static" without React Server Components use the streaming renderer to generate HTML. Tags rendered through expo-router/head are no longer included in the initial static HTML. The Head component still updates metadata after JavaScript loads.

Move metadata that must be present in the exported HTML to a generateMetadata() export in each route:

src/app/about.tsx
1import Head from 'expo-router/head';
1import type { Metadata } from 'expo-router/server';
22import { Text } from 'react-native';
33
4export function generateMetadata(): Metadata {
5 return {
6 title: 'About',
7 description: 'About our app',
8 };
9}
10
411export default function About() {
5 return (
6 <>
7 <Head>
8 <title>About</title>
9 <meta name="description" content="About our app" />
10 </Head>
11 <Text>About</Text>
12 </>
13 );
12 return <Text>About</Text>;
1413}

generateMetadata() can be asynchronous. For static rendering, its first argument is undefined and its second argument contains the route parameters. This also applies during development. Server rendering with web.output: "server" continues to provide the incoming request.

Metadata is resolved from the matched route, not its layouts. If a layout previously used Head for shared metadata, move that metadata into each route's generateMetadata() or add shared tags directly to the <head> in src/app/+html.tsx.

Move any <html> or <body> attributes previously set through Head onto those elements in your Root HTML component. For example, set lang on <html> in src/app/+html.tsx.

See Static rendering: Meta tags for the current metadata API.

Update custom Root HTML components

If your app uses web.output: "static" or "server" and has a src/app/+html.tsx file, make sure it calls useServerDocumentContext() from expo-router/html. Spread htmlAttributes and bodyAttributes onto their respective elements, and render headNodes inside <head> and bodyNodes after children inside <body>.

Without these nodes, the rendered HTML does not include generated metadata, styles, fonts, or JavaScript bundles. See the static rendering and server rendering examples.

The root HTML component also receives pathname and params props for the rendered route. Use these props instead of navigation hooks to customize HTML based on the route's pathname and URL parameters.

Remove deprecated server flags

Remove the following options from the expo-router config plugin in your app config. In SDK 58, they have no effect and emit a deprecation warning whenever present, including when set to false:

Deprecated optionSDK 58 behavior
unstable_useServerRenderingServer rendering runs when web.output is "server".
unstable_useServerDataLoadersData loaders are available with web.output: "static" or "server" without an opt-in.
unstable_useServerMiddlewareServer middleware no longer requires an opt-in.

Setting these flags to false no longer disables the corresponding feature. To keep prerendered HTML with API routes, follow Review server rendering behavior.

Review async route defaults

In SDK 58, async routes are enabled by default on web in development and production. Native defaults are unchanged, and native production builds continue to load routes synchronously.

Async routes use the default loading fallback instead of custom SuspenseFallback exports from layouts. If your web app relies on a custom fallback, disable async routes on web in the expo-router config plugin in your app config:

app.json
{ "expo": { "plugins": [["expo-router", { "asyncRoutes": { "web": false } }]] } }

This restores synchronous route loading and support for custom loading fallbacks on web. Existing explicit asyncRoutes settings continue to take precedence over the defaults. See Async routes for configuration options and limitations.

After upgrading or changing asyncRoutes, clear the Metro cache with expo start --clear or expo export --clear.

Move navigation hooks out of the root HTML

In SDK 58, navigation hooks, such as usePathname, useSegments, useLocalSearchParams, and useRouter, no longer work in the root HTML component in app/+html.tsx. Use them in the root layout in app/_layout.tsx instead.

Update Router tests

In SDK 58, expo-router/testing-library requires @testing-library/react-native version 14 or later. Update this dependency when upgrading your app.

renderRouter now returns a promise. Add await before rendering or calling the testRouter methods navigate, push, replace, back, setParams, and dismissAll so that updates finish before assertions run:

app.test.tsx
11import { renderRouter, screen, testRouter } from 'expo-router/testing-library';
22
3it('navigates to the profile', () => {
4 renderRouter(['index', 'profile']);
3it('navigates to the profile', async () => {
4 await renderRouter(['index', 'profile']);
55
6 testRouter.push('/profile');
6 await testRouter.push('/profile');
77
88expect(screen).toHavePathname('/profile');
99});

Replace imports and calls to renderRouterAsync with renderRouter, keeping await. Replace the removed RenderRouterAsyncOptions type with RenderRouterOptions. The testRouter.canGoBack() method remains synchronous.

See Testing with Expo Router for test helpers and Unit testing with Jest for setup instructions.

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.

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:

app/profile.tsx
1import { useNavigation } from 'expo-router';
1import { useRouter } from 'expo-router';
22
3const navigation = useNavigation();
3const router = useRouter();
44
5navigation.navigate('(tabs)', {
6 screen: 'feed',
5router.push({
6 pathname: '/(tabs)/feed/[id]',
77params: { id: '42' },
88});

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:

app/(tabs)/_layout.tsx
11import { Stack } from 'expo-router';
22
3export const unstable_settings = {
4 anchor: 'index',
5};
6
37export default function FeedLayout() {
4 return <Stack initialRouteName="index" />;
8 return <Stack />;
59}

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:

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:

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:

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:

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 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:

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:

Terminal
# Android md icons in native tabs
- npx expo install expo-symbols
# Android Stack.Toolbar
- npx expo install @expo/ui
# Android md icons in native tabs
- yarn expo install expo-symbols
# Android Stack.Toolbar
- yarn expo install @expo/ui
# Android md icons in native tabs
- pnpm expo install expo-symbols
# Android Stack.Toolbar
- pnpm expo install @expo/ui
# 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:

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.

Review the iOS header appearance

In SDK 58, Stack headers on iOS 26 and later use the system bar appearance by default. The header is transparent with Liquid Glass, and screen content goes under the header. The default value of headerTransparent is { ios: true, android: false, web: false }. On iOS, this default applies only when Liquid Glass is available and the screen does not set headerStyle.backgroundColor or headerBackground. Earlier iOS versions keep the opaque header. Apps that turn off Liquid Glass also keep the opaque header. Android headers are unchanged.

If content is hidden under the header, add contentInsetAdjustmentBehavior="automatic" to the root ScrollView, FlatList, or SectionList of the screen:

app/index.tsx
import { ScrollView, Text } from 'react-native'; export default function Home() { return ( <ScrollView contentInsetAdjustmentBehavior="automatic"> <Text>Home</Text> </ScrollView> ); }

For a screen without a scroll view, use SafeAreaView. To keep an opaque header on one screen, set headerTransparent: false in its options.

On iOS 26 and later, headers now use the system fonts instead of the theme fonts. To keep a custom font, set fontFamily in headerTitleStyle, headerBackTitleStyle, or headerLargeTitleStyle.

To restore the previous header appearance on iOS 26 and later for all screens, call enableLegacyHeaderAppearance() at the top level of the root layout:

app/_layout.tsx
import { enableLegacyHeaderAppearance, Stack } from 'expo-router'; enableLegacyHeaderAppearance(); export default function RootLayout() { return <Stack />; }

enableLegacyHeaderAppearance() is deprecated. In a future release, the native iOS header appearance becomes the only behavior and this function will be removed.

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:

navigation.ts
1navigation.dispatch(state =>
2 CommonActions.reset({
3 ...state,
4 index: 0,
5 routes: [state.routes[0]],
6 })
7);
1const state = navigation.getState();
2const action = CommonActions.reset({
3 ...state,
4 index: 0,
5 routes: [state.routes[0]],
6});
7
8navigation.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:

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:

SDK 57 APISDK 58 migration
getInitialStateRemove it. Expo Router builds initial state.
getRehydratedStateRemove it. Persisted data must provide complete state.
getStateForRouteNamesChangeInherit the base behavior, or handle ROUTE_NAMES_CHANGED in the extension.
routeParamListRemove it from RouterConfigOptions. Put parameter defaults in route code.
RouterActionOptionsUse RouterConfigOptions in getStateForAction.
Other router membersInherit 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:

custom-router.ts
1return nextState;
1// `affectedRoute` is selected by this action's handling logic.
2return {
3 state: nextState,
4 affectedRouteKey: affectedRoute?.key,
5};

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

Check handled actions with isActionHandled

Built-in routers and routers created with extendRouter or extendRouterActions now return RouterActionHandling.Unhandled instead of null when they do not handle an action. Replace null checks on a getStateForAction result with isActionHandled:

custom-router.ts
11const result = baseRouter.getStateForAction(state, action);
2if (result === null) {
2if (!isActionHandled(result)) {
33return result;
44}

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, RouterActionHandling, StackRouter, type CommonNavigationAction, type StackActionType, } from 'expo-router'; type CustomAction = { type: 'CUSTOM-ACTION'; payload: { name: string; params?: object }; }; const CustomStackRouter = extendRouter(StackRouter, ({ nextKey }) => ({ getStateForAction(state, action: CommonNavigationAction | StackActionType | CustomAction) { if (action.type === 'CUSTOM-ACTION') { if (!state.routeNames.includes(action.payload.name)) { return RouterActionHandling.Unhandled; } 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 RouterActionHandling.Delegate; }, }));

Use extendRouterActions when only getStateForAction needs customization. Its reducer returns the same values. Returning null or undefined instead of a RouterActionHandling value is deprecated and logs a warning in development.

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.

src/expo-router.ts
1import { withLayoutContext } from 'expo-router';
2import { createNavigator } from './navigator';
1import { createStandardRouterNavigator, TabRouter } from 'expo-router';
2import { TabNavigatorContent } from './navigator';
33
4export const Tabs = withLayoutContext(createNavigator().Navigator);
4export 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:

NavigatorcreateProps helper
Custom stackcreateBaseStackProps
Expo Router JavaScript stackcreateJSStackProps
Expo Router native stackcreateNativeStackProps
Custom tabcreateBaseTabProps
Expo Router JavaScript tabscreateJSTabsProps
Expo Router JavaScript top tabscreateJSTopTabsProps
Expo Router native tabscreateNativeTabsProps

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.

Removed APIReplacementChange
UNSTABLE_UnhandledLinkingContextNo app-level replacement. Expo Router owns unhandled link processing.#49616
BaseNavigationContainer, NavigationContainerLet Expo Router or ExpoRoot own the navigation container.#49587, #48760
Root options event, DocumentTitleOptions, documentTitleUse Expo Router Head or a <title> element for web metadata.#49590
onStateChange on container propsPrefer Expo Router state hooks, or listen for the navigation ref's state event.#49588
NavigationIndependentTree, useNavigationIndependentTreeUse NavigationContainer from @react-navigation/native for an isolated embedded navigation tree.#49172
React Navigation Link, LinkProps, useLinkPropsUse Link and LinkProps from expo-router with an href.#48895
navigateDeprecated, navigationInChildEnabledNavigate to a complete href with useRouter.#49102
NavigatorScreenParams, getActionFromState, LinkingOptions.getActionFromStateUse complete hrefs and let Expo Router resolve navigation state.#49297
resetRoot on navigation container refsUse router.replace or dispatch CommonActions.reset with complete state.#49297
beforeRemove, __unsafe_action__Use usePreventRemove, removePrevented, and removed.#49408
PreventRemoveContext, usePreventRemoveContext, PreventRemoveProviderUse usePreventRemove. Expo Router owns the provider.#49408, #48347
Router.getInitialState, Router.getRehydratedStateLet Expo Router build initial state and return complete state from custom routers.#48783, #49297
Router.getStateForRouteNamesChangeHandle ROUTE_NAMES_CHANGED in getStateForAction.#48479
RouterActionOptions, RouterConfigOptions.routeParamListUse RouterConfigOptions without routeParamList.#48783
DrawerNavigationState.defaultUse useDrawerStatus, or pass a default status to the deprecated getDrawerStatusFromState.#48750
Static navigation APIs and typesUse Expo Router file routes and layouts.#48071
LinkingOptions.enabledRemove the option. Expo Router owns linking.#49103
UNSTABLE_routeNamesChangeBehaviorUse protected route redirects and explicit href navigation.#47985
useOnlyUserDefinedScreensRemove the option. All filesystem routes remain registered.#47983