从 React 导航迁移

了解如何使用 React Navigation 将项目迁移到 Expo Router。


React Navigation 和 Expo Router 都是用于路由和导航的 Expo 框架。Expo Router 是对 React Navigation 的封装,许多概念都是相同的。

🌐 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.

投入

🌐 Pitch

除了 React Navigation 的所有优点外,Expo Router 还支持自动深度链接、类型安全延迟打包网页静态渲染 等功能。

🌐 Along with all the benefits of React Navigation, Expo Router enables automatic deep linking, type safety, deferred bundling, static rendering on web, and more.

反投入

🌐 Anti-pitch

如果你的应用使用自定义的 getPathFromStategetStateFromPath 组件,它可能不太适合 Expo Router。如果你使用这些功能来支持共享路由,那应该没问题,因为 Expo Router 对此有内置支持。

🌐 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.

建议

🌐 Recommendations

我们建议在开始迁移之前对你的代码库进行以下修改:

🌐 We recommend making the following modifications to your codebase before beginning the migration:

  • 将 React Navigation 的屏幕组件拆分到各自的文件中。例如,如果你有 <Stack.Screen component={HomeScreen} />,则确保 HomeScreen 组件在它自己的文件中。
  • 将项目转换为 TypeScript。这将更容易发现迁移过程中可能出现的错误。
  • 将相对导入转换为类型化别名。例如,在开始迁移之前,将 ../../components/button.tsx 转换为 @/components/button。这样在移动文件系统中的屏幕时,就不必更新相对路径了。
  • 迁移 away from resetRoot。这是在运行时“重启”应用的方式。这通常被认为是不好的做法,你应该重新构建应用的导航结构,以确保这种情况不再发生。
  • 将初始路由重命名为 index。Expo Router 会将启动时打开的路由视为匹配 /,React Navigation 用户通常会将初始路由命名为“Home”之类的名称。

重构搜索参数

🌐 Refactor search parameters

重构屏幕以使用可序列化的顶层查询参数。我们也建议在 React Navigation 中这样做。

🌐 Refactor screens to use serializable top-level query parameters. We recommend this in React Navigation as well.

在 Expo Router 中,搜索参数只能序列化顶层值,例如 numberbooleanstring。React Navigation 没有相同的限制,因此用户有时可以传递无效的参数,比如函数、对象、映射等。

🌐 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.

提前加载 UI

🌐 Eagerly load UI

在 React Native 应用中,当资源和字体加载时,从根组件 return null 是很常见的做法。这是不好的实践,并且在 Expo Router 中通常不被支持。如果你确实必须推迟渲染,那么确保不要尝试导航到任何屏幕。

🌐 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.

历史上,这种模式的存在是因为如果使用尚未加载的自定义字体,React Native 会抛出错误。我们在 React Native 0.72(SDK 49)中修改了这一上游行为,因此默认行为是在自定义字体加载后替换默认字体。如果你希望在字体加载完成之前隐藏单个文本元素,可以编写一个封装组件 <Text>,在字体加载完成之前返回 null。

🌐 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.

在网页上,从根返回 null 会导致静态渲染跳过所有子节点,结果没有可搜索的内容。可以通过在 Chrome 中使用“查看网页源代码”来测试,或者禁用 JavaScript 后重新加载页面。

🌐 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.

迁移

🌐 Migration

删除未使用或托管的代码

🌐 Delete unused or managed code

Expo Router 会自动添加 react-native-safe-area-context 支持。

🌐 Expo Router automatically adds react-native-safe-area-context support.

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.

Copy screens to the app directory

Create an app directory inside a root src directory. Expo Router automatically detects the src/app directory as the root of your routes.

Ensure your tsconfig.json and app.json are properly configured

tsconfig.json
{ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "paths": { "@/*": ["./src/*"], "@/assets/*": ["./assets/*"] } }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] }

Also ensure your app.json has the expo-router plugin configured:

app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": ["expo-router"] } }

Layout the structure of your app by creating files according to the application of Expo Router rules. Kebab-case and lowercase letters are considered best practice for route filenames.

Replace navigators with directories, for example:

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

  • Rename the "main" route from Home to index to ensure it matches the / path.
  • Convert names to lowercase.
  • Move all the screens to the appropriate file locations inside the app directory. This may take some experimenting.
src
app
  _layout.tsx
  (home)
   _layout.tsx
   index.tsx
   feed.tsx
  profile.tsx
  settings.tsx
src/app/_layout.tsx
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.

src/app/(home)/_layout.tsx
import { Tabs } from 'expo-router'; export default function HomeLayout() { return <Tabs />; }

使用 Expo Router 钩子

🌐 Use Expo Router hooks

React Navigation v6 及以下版本会将 { navigation, route } 属性传递给每个屏幕。这个模式在 React Navigation 中将不再使用,并且我们从未在 Expo Router 中引入过它。

🌐 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.

相反,将 navigation 迁移到 useRouter 钩子。

🌐 Instead, migrate navigation to the useRouter hook.

同样地,将 route 属性迁移到 useLocalSearchParams 钩子。

🌐 Similarly, migrate from the route prop to the useLocalSearchParams hook.

要访问 navigation.navigate,请从 useNavigation 钩子中导入 navigation 属性。

🌐 To access the navigation.navigate, import the navigation prop from useNavigation hook.

迁移链接组件

🌐 Migrate the Link component

React Navigation 和 Expo Router 都提供 Link 组件。然而,Expo 的 Link 组件使用 href 而不是 to

🌐 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 用户通常会使用 useLinkProps 钩子创建自定义 Link 组件以控制子组件。在 Expo Router 中这不是必需的,相反,可以使用 asChild 属性。

🌐 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.

跨导航器共享屏幕

🌐 Share screens across navigators

在 React Navigation 应用中,在多个导航器中重用一组路由是很常见的。这通常与标签页一起使用,以确保每个标签页都可以推送任何屏幕。

🌐 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.

在 Expo Router 中,你可以迁移到 共享路由,或者创建多个文件并从中重新导出相同的组件。

🌐 In Expo Router, you can either migrate to shared routes or create multiple files and re-export the same component from them.

当你使用组或共享路由时,你可以通过使用完全限定的路由名称来导航到特定的选项卡,例如使用 /(home)/settings 而不是 /settings

🌐 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.

迁移屏幕跟踪事件

🌐 Migrate screen tracking events

你可以根据我们的React Navigation 屏幕追踪指南设置屏幕追踪,然后根据Expo Router 屏幕追踪指南进行更新。

🌐 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.

使用特定于平台的屏幕组件

🌐 Use platform-specific components for screens

请参阅特定平台模块指南,以获取有关根据平台切换界面的信息。

🌐 Refer to the platform-specific modules guide for info on switching UI based on the platform.

替换 NavigationContainer

🌐 Replace the NavigationContainer

全球的 React Navigation <NavigationContainer /> 完全由 Expo Router 管理。Expo Router 提供了实现与 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.

API 替代

参考

🌐 Ref

NavigationContainer 引用不应被直接访问。请改用以下方法。

🌐 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

使用 useRootNavigationState()

🌐 Use useRootNavigationState().

getCurrentRoute

与 React Navigation 不同,Expo Router 可以可靠地用字符串表示任何路由。使用 usePathname()useSegments() 钩子来识别当前路由。

🌐 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

使用 useLocalSearchParams() 钩子获取当前路由的查询参数。

🌐 Use the useLocalSearchParams() hook to get the current route's query parameters.

addListener

以下事件可以迁移:

🌐 The following events can be migrated:

state

使用 usePathname()useSegments() 钩子来识别当前路由。与 useEffect(() => {}, [...]) 一起使用以观察变化。

🌐 Use the usePathname() or useSegments() hooks to identify the current route. Use in conjunction with useEffect(() => {}, [...]) to observe changes.

options

使用 useLocalSearchParams() 钩子获取当前路由的查询参数。与 useEffect(() => {}, [...]) 一起使用以观察变化。

🌐 Use the useLocalSearchParams() hook to get the current route's query parameters. Use in conjunction with useEffect(() => {}, [...]) to observe changes.

props

迁移以下 <NavigationContainer /> 属性:

🌐 Migrate the following <NavigationContainer /> props:

initialState

在 Expo Router 中,你可以从路由字符串(例如 /user/evanbacon)重新加载你的应用状态。使用 重定向 来处理初始状态。有关高级重定向,请参阅 共享路由

🌐 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.

避免使用这种模式,建议使用深度链接(例如,用户打开你的应用直接进入 /profile 而不是从主页进入),因为它最类似于网页。如果应用因某个特定页面而崩溃,最好避免在应用启动时自动导航回该页面,因为这可能需要重新安装应用才能修复。

🌐 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

使用 usePathname()useSegments()useGlobalSearchParams() 钩子来识别当前路由状态。与 useEffect(() => {}, [...]) 一起使用以观察变化。

🌐 Use the usePathname(), useSegments(), and useGlobalSearchParams() hooks to identify the current route state. Use in conjunction with useEffect(() => {}, [...]) to observe changes.

onReady

在 React Navigation 中,onReady 通常用于确定启动屏幕何时隐藏或何时使用分析工具跟踪屏幕。Expo Router 对这两种用例都有特殊处理。在 Expo Router 中,可以假设导航始终准备好处理导航事件。

🌐 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

在 Expo Router 中,操作总是会被处理。使用动态路由404 页面来替代onUnhandledAction

🌐 Actions are always handled in Expo Router. Use dynamic routes and 404 screens in favor of onUnhandledAction.

linking

[linking](https://react-navigation.nodejs.cn/docs/navigation-container/#linking) 属性会根据 app 目录中的文件自动构建。

🌐 The linking prop is automatically constructed based on the files to the app directory.

fallback

fallback 属性由 Expo Router 自动处理。更多信息请参阅 启动屏幕 文档。

🌐 The fallback prop is automatically handled by Expo Router. Learn more in the Splash Screen reference.

theme

在 React Navigation 中,你可以使用 <NavigationContainer /> 组件为整个应用设置主题。Expo Router 会帮你管理根容器,因此你应直接使用 ThemeProvider 来设置主题。

🌐 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.

src/app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme, useTheme } from '@react-navigation/native'; import { Slot } from 'expo-router'; export default function RootLayout() { return ( <ThemeProvider value={DarkTheme}> <Slot /> </ThemeProvider> ); }

你可以在应用的任何层使用此技术为特定布局设置主题。可以通过 @react-navigation/nativeuseTheme 钩子访问当前主题。

🌐 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

children 属性会根据 app 目录中的文件以及当前打开的 URL 自动填充。

🌐 The children prop is automatically populated based on the files in the app directory and the currently open URL.

independent

Expo Router 不支持 independent 容器。这是因为路由负责管理单个 <NavigationContainer />。任何额外的容器都不会被 Expo Router 自动管理。

🌐 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

使用 Head 组件 设置网页标题。

🌐 Use the Head component to set the webpage title.

ref

改用 useNavigationContainerRef() 钩子。

🌐 Use the useNavigationContainerRef() hook instead.

重写自定义导航器

🌐 Rewrite custom navigators

如果你的项目有自定义导航器,你可以重写它或将其移植到 Expo Router。

🌐 If your project has a custom navigator, you can rewrite this or port it to Expo Router.

要移植,只需使用 withLayoutContext 函数:

🌐 To port, simply use the withLayoutContext function:

import { createCustomNavigator } from './my-navigator'; export const CustomNavigator = withLayoutContext(createCustomNavigator().Navigator);

要重写,请使用 Navigator 组件,它封装了来自 React Navigation 的 useNavigationBuilder 钩子。

🌐 To rewrite, use the Navigator component, which wraps the useNavigationBuilder hook from React Navigation.

useNavigationBuilder 的返回值可以通过 <Navigator /> 组件内部的 Navigator.useContext() 钩子访问。属性可以使用 <Navigator /> 组件的 props 传递给 useNavigationBuilder,这包括 initialRouteNamescreenOptionsrouter

🌐 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.

<Navigator /> 组件的所有 children 都将按原样渲染。

🌐 All of the children of a <Navigator /> component will be rendered as-is.

  • Navigator.useContext:访问 React Navigation 的 statenavigationdescriptorsrouter 以使用自定义导航器。
  • Navigator.Slot:一个用于渲染当前选定路由的 React 组件。此组件只能在 <Navigator /> 组件内渲染。

示例

🌐 Example

自定义布局有一个内部上下文,当使用 <Slot /> 组件而没有 <Navigator /> 组件封装时,该上下文会被忽略。

🌐 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 路由的启动画面封装器

🌐 Use Expo Router's Splash Screen wrapper

Expo Router 封装了 expo-splash-screen 并添加了特殊处理,以确保在导航挂载后以及捕获到意外错误时将其隐藏。只需将导入 expo-splash-screen 的方式迁移为从 expo-router 导入 SplashScreen 即可。

🌐 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.

导航状态监控

🌐 Navigation state observation

如果你正在直接观察导航状态,请迁移到 usePathnameuseSegmentsuseGlobalSearchParams 钩子。

🌐 If you're observing the navigation state directly, migrate to the usePathname, useSegments, and useGlobalSearchParams hooks.

将参数传递给嵌套屏幕

🌐 Pass params to nested screens

不要使用 嵌套屏幕导航事件,请使用合格的 href:

🌐 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' } });

为深度链接和服务器导航设置初始路由

🌐 Set initial routes for deep linking and server navigation

在 React Navigation 中,你可以使用链接配置的 initialRouteName 属性。在 Expo Router 中,使用 布局设置

🌐 In React Navigation, you can use the initialRouteName property of the linking configuration. In Expo Router, use layout settings.

重置导航状态

🌐 Reset navigation state

你可以使用 React Navigation 库中的 reset 操作来重置导航状态。它通过 Expo Router 中的 useNavigation 钩子分发,以访问 navigation 属性。

🌐 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.

在下面的示例中,navigation 属性可以通过 useNavigation 钩子访问,而 CommonActions.reset 动作可以通过 @react-navigation/native 使用。reset 动作中指定的对象会用新的对象替换现有的导航状态。

🌐 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.

src/app/screen.tsx
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='重置' onPress={handleResetAction} /> </> ); }

迁移 TypeScript 类型

🌐 Migrate TypeScript types

Expo Router 可以自动生成静态类型的路由,这将确保你只能导航到有效的路由。

🌐 Expo Router can automatically generate statically typed routes, this will ensure you can only navigate to valid routes.

附加信息

🌐 Additional information

React 导航主题

🌐 React Navigation themes

React Navigation 导航器 <Stack><Drawer><Tabs> 使用共享的外观提供者。在 React Navigation 中,你可以使用 <NavigationContainer /> 组件为整个应用设置主题。Expo Router 管理根容器,因此你可以直接使用 ThemeProvider 设置主题。

🌐 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.

src/app/_layout.tsx
import { ThemeProvider, DarkTheme, DefaultTheme, useTheme } from '@react-navigation/native'; import { Slot } from 'expo-router'; export default function RootLayout() { return ( <ThemeProvider value={DarkTheme}> <Slot /> </ThemeProvider> ); }

你可以在应用的任何层使用这种技术来为特定布局设置主题。当前主题可以通过 @react-navigation/native 提供的 useTheme 钩子访问。

🌐 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.

React 导航元素

🌐 React Navigation Elements

@react-navigation/elements 库提供了一组可用于构建导航界面的 UI 元素和辅助工具。这些组件设计为可组合和可定制。你可以重用库中的默认功能,也可以在其基础上构建自己的导航界面。

🌐 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.

要在 Expo Router 中使用它,你需要安装该库:

🌐 To use it with Expo Router, you need to install the library:

Terminal
npx expo install @react-navigation/elements

要了解该库提供的组件和工具,请参阅 Elements library 文档。

🌐 To learn more about the components and utilities the library provides, see Elements library documentation.