Edit this page
Learn how to migrate a project using React Navigation to Expo Router.
Both React Navigation and Expo Router are Expo frameworks for routing and navigation. Expo Router is a wrapper around React Navigation and many of the concepts are the same.
Along with all the benefits of React Navigation, Expo Router enables automatic deep linking, type safety, deferred bundling, static rendering on web, and more.
If your app uses a custom getPathFromState
or getStateFromPath
component, it may not be a good fit for Expo Router. If you're using these functions to support shared routes then you should be fine as Expo Router has built-in support for this.
We recommend making the following modifications to your codebase before beginning the migration:
<Stack.Screen component={HomeScreen} />
, then ensure the HomeScreen
class is in its own file.../../components/button.tsx
to @/components/button
before starting the migration. This makes it easier to move screens around the filesystem without having to update the relative paths.resetRoot
. This is used to "restart" the app while running. This is generally considered bad practice, and you should restructure your app's navigation so this never needs to happen.index
. Expo Router considers the route that is opened on launch to match /
, React Navigation users will generally use something such as "Home" for the initial route.Refactor screens to use serializable top-level query parameters. We recommend this in React Navigation as well.
In Expo Router, search parameters can only serialize top-level values such as number
, boolean
, and string
. React Navigation doesn't have the same restrictions, so users can sometimes pass invalid parameters like Functions, Objects, Maps, and so on.
If your code has something similar to the below:
import { useNavigation } from '@react-navigation/native';
const navigation = useNavigation();
navigation.push('Followers', {
onPress: profile => {
navigation.push('User', { profile });
},
});
Consider restructuring so the function can be accessed from the "followers" screen. In this case, you can access the router and push directly from the "followers" screen.
It's common in React Native apps to return null
from the root component while assets and fonts are loading. This is bad practice and generally unsupported in Expo Router. If you absolutely must defer rendering, then ensure you don't attempt to navigate to any screens.
Historically this pattern exists because React Native will throw errors if you use custom fonts that haven't loaded yet. We changed this upstream in React Native 0.72 (SDK 49) so the default behavior is to swap the default font when the custom font loads. If you'd like to hide individual text elements until a font has finished loading, write a wrapper <Text>
, which returns null until the font has loaded.
On web, returning null
from the root will cause static rendering to skip all of the children, resulting in no searchable content. This can be tested by using "View Page Source" in Chrome, or by disabling JavaScript and reloading the page.
Expo Router automatically adds react-native-safe-area-context
support.
- import { SafeAreaProvider } from "react-native-safe-area-context";
export default function App() {
return (
- <SafeAreaProvider>
<MyApp />
- </SafeAreaProvider>
)
}
Expo Router does not add react-native-gesture-handler
(as of v3), so you'll have to add this yourself if you are using Gesture Handler or <Drawer />
layout. Avoid using this package on web since it adds a lot of JavaScript that is often unused.
Create an app directory at the root of your repo, or in a root src directory.
Layout the structure of your app by creating files according to the creating pages guide. Kebab-case and lowercase letters are considered best practice for route filenames.
Replace navigators with directories, for example:
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen name="Feed" component={Feed} />
</Tab.Navigator>
);
}
function App() {
return (
// NavigationContainer is managed by Expo Router.
<NavigationContainer
linking={
{
// ...linking configuration
}
}
>
<Stack.Navigator>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen
name="Home"
component={HomeTabs}
options={{
title: 'Home Screen',
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
Expo Router:
/
path.app
_layout.js
(home)
_layout.js
index.js
feed.js
profile.js
settings.js
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen
name="(home)"
options={
{
title: 'Home Screen',
}
}
/>
</Stack>
);
}
The tab navigator will be moved to a subdirectory.
import { Tabs } from 'expo-router';
export default function HomeLayout() {
return <Tabs />;
}
React Navigation v6 and lower will pass the props { navigation, route }
to every screen. This pattern is going away in React Navigation, and we never introduced it to the Expo Router.
Instead, migrate navigation
to the useRouter
hook.
+ import { useRouter } from 'expo-router';
export default function Page({
- navigation
}) {
- navigation.push('User', { user: 'bacon' });
+ const router = useRouter();
+ router.push('/users/bacon');
}
Similarly, migrate from the route
prop to the useLocalSearchParams
hook.
+ import { useLocalSearchParams } from 'expo-router';
export default function Page({
- route
}) {
- const user = route?.params?.user;
+ const { user } = useLocalSearchParams();
}
To access the navigation.navigate
, import the navigation
prop from useNavigation
hook.
+ import { useNavigation } from 'expo-router';
export default function Page({
+ const navigation = useNavigation();
return (
<Button onPress={navigation.navigate('screenName')}>
)
})
React Navigation and Expo Router both provide Link components. However, Expo's Link component uses href
instead of to
.
// React Navigation
<Link to="Settings" />
// Expo Router
<Link href="/settings" />
React Navigation users will often create a custom Link component with the useLinkProps
hook to control the child component. This isn't necessary in Expo Router, instead, use the asChild
prop.
It's common for React Navigation apps to reuse a set of routes across multiple navigators. This is generally used with tabs to ensure each tab can push any screen.
In Expo Router, you can either migrate to shared routes or create multiple files and re-export the same component from them.
When you use groups or shared routes, you can navigate to specific tabs by using the fully qualified route name, for example, /(home)/settings
instead of /settings
.
You may have your screen tracking setup according to our React Navigation screen tracking guide, update it according to the Expo Router screen tracking guide.
Refer to the platform-specific modules guide for info on switching UI based on the platform.
NavigationContainer
The global React Navigation <NavigationContainer />
is completely managed in Expo Router. Expo Router provides systems for achieving the same functionality as the NavigationContainer
without needing to use it directly.
The NavigationContainer
ref should not be accessed directly. Use the following methods instead.
resetRoot
Navigate to the initial route of the application. For example, if your app starts at /
(recommended), then you can replace the current route with /
using this method.
import { useRouter } from 'expo-router';
function Example() {
const router = useRouter();
return (
<Text
onPress={() => {
// Go to the initial route of the application.
router.replace('/');
}}>
Reset App
</Text>
);
}
getRootState
Use useRootNavigationState()
.
getCurrentRoute
Unlike React Navigation, Expo Router can reliably represent any route with a string. Use the usePathname()
or useSegments()
hooks to identify the current route.
getCurrentOptions
Use the useLocalSearchParams()
hook to get the current route's query parameters.
addListener
The following events can be migrated:
state
Use the usePathname()
or useSegments()
hooks to identify the current route. Use in conjunction with useEffect(() => {}, [...])
to observe changes.
options
Use the useLocalSearchParams()
hook to get the current route's query parameters. Use in conjunction with useEffect(() => {}, [...])
to observe changes.
Migrate the following <NavigationContainer />
props:
initialState
In Expo Router, you can rehydrate your application state from a route string (for example, /user/evanbacon
). Use redirects to handle initial states. See shared routes for advanced redirects.
Avoid using this pattern in favor of deep linking (for example, a user opens your app to /profile
rather than from the home screen) as it is most analogous to the web. If an app crashes due to a particular screen, it's best to avoid automatically navigating back to that exact screen when the app starts as it may require reinstalling the app to fix.
onStateChange
Use the usePathname()
, useSegments()
, and useGlobalSearchParams()
hooks to identify the current route state. Use in conjunction with useEffect(() => {}, [...])
to observe changes.
onStateChange
.onReady
In React Navigation, onReady
is most often used to determine when the splash screen should hide or when to track screens using analytics. Expo Router has special handling for both of these use cases. Assume the navigation is always ready for navigation events in the Expo Router.
onUnhandledAction
Actions are always handled in Expo Router. Use dynamic routes and 404 screens in favor of onUnhandledAction
.
linking
The linking
prop is automatically constructed based on the files to the app directory.
fallback
The fallback
prop is automatically handled by Expo Router. Learn more in the Splash Screen reference.
theme
In React Navigation, you set the theme for the entire app using the <NavigationContainer />
component. Expo Router manages the root container for you, so instead you should set the theme using the ThemeProvider
directly.
import { ThemeProvider, DarkTheme, DefaultTheme, useTheme } from '@react-navigation/native';
import { Slot } from 'expo-router';
export default function RootLayout() {
return (
<ThemeProvider value={DarkTheme}>
<Slot />
</ThemeProvider>
);
}
You can use this technique at any layer of the app to set the theme for a specific layout. The current theme can be accessed with the useTheme
hook from @react-navigation/native
.
children
The children
prop is automatically populated based on the files in the app directory and the currently open URL.
independent
Expo Router does not support independent
containers. This is because the router is responsible for managing the single <NavigationContainer />
. Any additional containers will not be automatically managed by Expo Router.
documentTitle
Use the Head component to set the webpage title.
ref
Use the useNavigationContainerRef()
hook instead.
If your project has a custom navigator, you can rewrite this or port it to Expo Router.
To port, simply use the withLayoutContext
function:
import { createCustomNavigator } from './my-navigator';
export const CustomNavigator = withLayoutContext(createCustomNavigator().Navigator);
To rewrite, use the Navigator
component, which wraps the useNavigationBuilder
hook from React Navigation.
The return value of useNavigationBuilder
can be accessed with the Navigator.useContext()
hook from inside the <Navigator />
component. Properties can be passed to useNavigationBuilder
using the props of the <Navigator />
component, this includes initialRouteName
, screenOptions
, router
.
All of the children
of a <Navigator />
component will be rendered as-is.
Navigator.useContext
: Access the React Navigation state
, navigation
, descriptors
, and router
for the custom navigator.Navigator.Slot
: A React component used to render the currently selected route. This component can only be rendered inside a <Navigator />
component.Custom layouts have an internal context that is ignored when using the <Slot />
component without a <Navigator />
component wrapping it.
import { View } from 'react-native';
import { TabRouter } from '@react-navigation/native';
import { Navigator, usePathname, Slot, Link } from 'expo-router';
export default function App() {
return (
<Navigator router={TabRouter}>
<Header />
<Slot />
</Navigator>
);
}
function Header() {;
const pathname = usePathname();
return (
<View>
<Link href="/">Home</Link>
<Link
href="/profile"
style={[pathname === '/profile' && { color: 'blue' }]}>
Profile
</Link>
<Link href="/settings">Settings</Link>
</View>
);
}
Expo Router wraps expo-splash-screen
and adds special handling to ensure it's hidden after the navigation mounts, and whenever an unexpected error is caught. Simply migrate from importing expo-splash-screen
to importing SplashScreen
from expo-router
.
If you're observing the navigation state directly, migrate to the usePathname
, useSegments
, and useGlobalSearchParams
hooks.
Instead of using the nested screen navigation events, use a qualified href:
// React Navigation
navigation.navigate('Account', {
screen: 'Settings',
params: { user: 'jane' },
});
// Expo Router
router.push({ pathname: '/account/settings', params: { user: 'jane' } });
In React Navigation, you can use the initialRouteName
property of the linking configuration. In Expo Router, use layout settings.
You can use the reset
action from the React Navigation library to reset the navigation state. It is dispatched using the useNavigation
hook from Expo Router to access the navigation
prop.
In the below example, the navigation
prop is accessible from the useNavigation
hook and the CommonActions.reset
action from @react-navigation/native
. The object specified in the reset
action replaces the existing navigation state with the new one.
import { useNavigation } from 'expo-router'
import { CommonActions } from '@react-navigation/native'
export default function Screen() {
const navigation = useNavigation();
const handleResetAction = () => {
navigation.dispatch(CommonActions.reset({
routes: [{key: "(tabs)", name: "(tabs)"}]
}))
}
return (
<>
{/* ...rest of the code */}
<Button title='Reset' onPress={handleResetAction} />
</>
);
}
Expo Router can automatically generate statically typed routes, this will ensure you can only navigate to valid routes.
React Navigation navigators <Stack>
, <Drawer>
, and <Tabs>
use a shared appearance provider. In React Navigation, you set the theme for the entire app using the <NavigationContainer />
component. Expo Router manages the root container so that you can set the theme using the ThemeProvider
directly.
import { ThemeProvider, DarkTheme, DefaultTheme, useTheme } from '@react-navigation/native';
import { Slot } from 'expo-router';
export default function RootLayout() {
return (
<ThemeProvider value={DarkTheme}>
<Slot />
</ThemeProvider>
);
}
You can use this technique at any layer of the app to set the theme for a specific layout. The current theme can be accessed via useTheme
hook from @react-navigation/native
.
The @react-navigation/elements
library provides a set of UI elements and helpers that can be used to build a navigation UI. These components are designed to be composable and customizable. You can reuse the default functionality from the library or build your navigator's UI on top of it.
To use it with Expo Router, you need to install the library:
-
npm install @react-navigation/elements
-
yarn add @react-navigation/elements
To learn more about the components and utilities the library provides, see Elements library documentation.