从 React Navigation 迁移
编辑页面
了解如何将使用 React Navigation 的项目迁移到 Expo Router。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
React Navigation 和 Expo Router 都是用于路由和导航的 Expo 框架。Expo Router 是对 React Navigation 的一层封装,许多概念都是相同的。
亮点
除了 React Navigation 的所有优点之外,Expo Router 还支持自动深度链接、类型安全、延迟打包、Web 端静态渲染等功能。
不适合的情况
如果你的应用使用了自定义的 getPathFromState 或 getStateFromPath 组件,那么它可能并不适合 Expo Router。如果你使用这些函数是为了支持共享路由,那就没问题,因为 Expo Router 已内置对此的支持。
建议
我们建议在开始迁移之前,对代码库进行以下修改:
- 将 React Navigation 的屏幕组件拆分到各自独立的文件中。例如,如果你有
<Stack.Screen component={HomeScreen} />,请确保HomeScreen组件位于它自己的文件中。 - 将项目转换为 TypeScript。这将有助于更容易发现迁移过程中可能出现的错误。
- 将相对导入转换为 类型化别名。例如,在开始迁移前,将
../../components/button.tsx改为@/components/button。这样可以更轻松地在文件系统中移动屏幕,而无需更新相对路径。 - 迁移掉
resetRoot。它用于在运行时“重启”应用。通常这被认为是不好的实践,你应该重构应用的导航结构,使其不再需要这样做。 - 将初始路由重命名为
index。Expo Router 认为启动时打开的路由对应/,而 React Navigation 用户通常会将初始路由命名为类似 “Home”。
重构搜索参数
将屏幕重构为使用可序列化的顶层查询参数。我们也建议在 React Navigation 中这样做。
在 Expo Router 中,搜索参数只能序列化顶层值,例如 number、boolean 和 string。React Navigation 没有相同的限制,因此用户有时会传入无效参数,例如 Functions、Objects、Maps 等。
如果你的代码类似下面这样:
import { useNavigation } from '@react-navigation/native'; const navigation = useNavigation(); navigation.push('Followers', { onPress: profile => { navigation.push('User', { profile }); }, });
可以考虑重构,使这个函数能够从 “followers” 屏幕中访问到。在这种情况下,你可以在 “followers” 屏幕中直接访问 router 并进行 push。
提前加载 UI
在 React Native 应用中,根组件在资源和字体加载期间 return null 很常见。这是不好的实践,并且通常不受 Expo Router 支持。如果你必须延迟渲染,请确保不要尝试导航到任何屏幕。
这种模式之所以长期存在,是因为如果使用尚未加载完成的自定义字体,React Native 会抛出错误。我们在 React Native 0.72(SDK 49)中向上游进行了更改,因此默认行为是在自定义字体加载完成后替换默认字体。如果你希望在字体加载完成之前隐藏单个文本元素,可以编写一个包装 <Text>,在字体尚未加载完成时返回 null。
在 Web 上,如果根组件返回 null,将导致静态渲染跳过所有子组件,从而没有可搜索内容。你可以通过在 Chrome 中使用“查看网页源代码”,或禁用 JavaScript 后重新加载页面来进行测试。
迁移
删除未使用或由管理代码生成的内容
Expo Router 会自动添加 react-native-safe-area-context 支持。
Expo Router 不会添加 react-native-gesture-handler(截至 v3),因此如果你正在使用 Gesture Handler 或 <Drawer /> 布局,你需要自己添加它。由于它会加入很多通常未使用的 JavaScript,建议在 web 上避免使用这个包。
将屏幕复制到 app 目录
在根 src 目录中创建一个 app 目录。Expo Router 会自动检测 src/app 目录作为你的路由根目录。
确保你的 tsconfig.json 和 app.json 配置正确
{ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "paths": { "@/*": ["./src/*"], "@/assets/*": ["./assets/*"] } }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] }
同时确保你的 app.json 已配置 expo-router 插件:
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": ["expo-router"] } }
按照将 Expo Router 规则应用到应用中来组织你的应用结构。对于路由文件名,kebab-case 和小写字母被视为最佳实践。
将导航器替换为目录,例如:
function HomeTabs() { return ( <Tab.Navigator> <Tab.Screen name="Home" component={Home} /> <Tab.Screen name="Feed" component={Feed} /> </Tab.Navigator> ); } function App() { return ( // NavigationContainer 由 Expo Router 管理。 <NavigationContainer linking={ { // ...linking 配置 } } > <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:
- 将 “main” 路由从 Home 重命名为 index,以确保它与
/路径匹配。 - 将名称转换为小写。
- 将所有屏幕移动到 app 目录内相应的文件位置。这可能需要一些尝试。
srcapp_layout.tsx(home)_layout.tsxindex.tsxfeed.tsxprofile.tsxsettings.tsximport { Stack } from 'expo-router'; export default function RootLayout() { return ( <Stack> <Stack.Screen name="(home)" options={ { title: 'Home Screen', } } /> </Stack> ); }
标签导航器将被移动到一个子目录中。
import { Tabs } from 'expo-router'; export default function HomeLayout() { return <Tabs />; }
使用 Expo Router hooks
React Navigation v6 及更早版本会将 props { navigation, route } 传递给每个屏幕。这种模式在 React Navigation 中正在被移除,而我们从未将其引入 Expo Router。
相反,请将 navigation 迁移为 useRouter hook。
同样,将 route prop 迁移为 useLocalSearchParams hook。
要访问 navigation.navigate,请从 useNavigation hook 中导入 navigation prop。
迁移 Link 组件
React Navigation 和 Expo Router 都提供 Link 组件。不过,Expo 的 Link 组件使用 href,而不是 to。
// React Navigation <Link to="Settings" /> // Expo Router <Link href="/settings" />
React Navigation 用户通常会使用 useLinkProps hook 创建自定义 Link 组件来控制子组件。在 Expo Router 中这不是必需的,改用 asChild prop 即可。
在导航器之间共享屏幕
React Navigation 应用通常会在多个导航器之间复用一组路由。通常会与标签页一起使用,以确保每个标签页都可以推入任意屏幕。
在 Expo Router 中,你可以迁移到共享路由,或者创建多个文件并从中重新导出同一个组件。
当你使用分组或共享路由时,可以通过使用完整限定的路由名称来导航到特定标签页,例如使用 /(home)/settings 而不是 /settings。
迁移屏幕跟踪事件
你可能已经按照我们的 React Navigation 屏幕跟踪指南 配置了屏幕跟踪,请根据 Expo Router 屏幕跟踪指南进行更新。
为屏幕使用特定平台组件
有关根据平台切换 UI 的信息,请参阅特定平台模块指南。
替换 NavigationContainer
Expo Router 完全管理全局 React Navigation <NavigationContainer />。Expo Router 提供了一些系统,可实现与 NavigationContainer 相同的功能,而无需直接使用它。
API 替代项
Ref
不应直接访问 NavigationContainer ref。请改用以下方法。
resetRoot
导航到应用的初始路由。例如,如果你的应用从 / 开始(推荐),则可以使用此方法将当前路由替换为 /。
import { useRouter } from 'expo-router'; function Example() { const router = useRouter(); return ( <Text onPress={() => { // 转到应用的初始路由。 router.replace('/'); }}> 重置应用 </Text> ); }
getRootState
使用 useRootNavigationState()。
getCurrentRoute
与 React Navigation 不同,Expo Router 可以可靠地用字符串表示任何路由。使用 usePathname() 或 useSegments() hooks 来识别当前路由。
getCurrentOptions
使用 useLocalSearchParams() hook 获取当前路由的查询参数。
addListener
以下事件可以迁移:
state
使用 usePathname() 或 useSegments() hooks 来识别当前路由。结合 useEffect(() => {}, [...]) 使用以观察变化。
options
使用 useLocalSearchParams() hook 获取当前路由的查询参数。结合 useEffect(() => {}, [...]) 使用以观察变化。
props
迁移以下 <NavigationContainer /> props:
initialState
在 Expo Router 中,你可以从路由字符串重新恢复应用状态(例如,/user/evanbacon)。使用 重定向 处理初始状态。有关高级重定向,请参阅共享路由。
建议避免使用这种模式,而是采用深度链接(例如,用户直接打开你的应用到 /profile,而不是从首页打开),因为它与 Web 最为相似。如果某个特定屏幕导致应用崩溃,最好避免在应用启动时自动导航回那个确切的屏幕,因为这可能需要重新安装应用才能修复。
onStateChange
使用 usePathname()、useSegments() 和 useGlobalSearchParams() hooks 来识别当前路由状态。结合 useEffect(() => {}, [...]) 使用以观察变化。
- 如果你尝试跟踪屏幕变化,请遵循屏幕跟踪指南。
- React Navigation 建议避免使用
onStateChange。
onReady
在 React Navigation 中,onReady 最常用于确定启动屏幕何时应隐藏,或使用分析工具跟踪屏幕。Expo Router 对这两种用例都有特殊处理。请假设在 Expo Router 中导航始终已准备好接收导航事件。
onUnhandledAction
在 Expo Router 中,动作始终会被处理。请使用动态路由和404 屏幕来替代 onUnhandledAction。
linking
linking prop 会根据 app 目录中的文件自动构建。
fallback
fallback prop 由 Expo Router 自动处理。有关更多信息,请参阅 Splash Screen 参考文档。
theme
在 React Navigation 中,你使用 <NavigationContainer /> 组件为整个应用设置主题。Expo Router 会为你管理根容器,因此你应该直接使用 ThemeProvider 设置主题。
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 hook 访问。
children
children prop 会根据 app 目录中的文件以及当前打开的 URL 自动填充。
independent
Expo Router 不支持 independent 容器。这是因为路由器负责管理单一的 <NavigationContainer />。任何额外的容器都不会被 Expo Router 自动管理。
documentTitle
使用 Head 组件设置网页标题。
ref
请改用 useNavigationContainerRef() hook。
重写自定义导航器
如果你的项目有自定义导航器,你可以重写它,或将其移植到 Expo Router。
要进行移植,只需使用 withLayoutContext 函数:
import { createCustomNavigator } from './my-navigator'; export const CustomNavigator = withLayoutContext(createCustomNavigator().Navigator);
要重写,请使用 Navigator 组件,它封装了 React Navigation 中的 useNavigationBuilder hook。
useNavigationBuilder 的返回值可以从 <Navigator /> 组件内部通过 Navigator.useContext() hook 访问。属性可以通过 <Navigator /> 组件的 props 传递给 useNavigationBuilder,这包括 initialRouteName、screenOptions、router。
<Navigator /> 组件的所有 children 都将按原样渲染。
Navigator.useContext:访问自定义导航器的 React Navigationstate、navigation、descriptors和router。Navigator.Slot:用于渲染当前所选路由的 React 组件。此组件只能在<Navigator />组件内部渲染。
示例
自定义布局具有一个内部上下文,当在没有用 <Navigator /> 组件包裹的情况下使用 <Slot /> 组件时,该上下文会被忽略。
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="/">首页</Link> <Link href="/profile" style={[pathname === '/profile' && { color: 'blue' }]}> 个人资料 </Link> <Link href="/settings">设置</Link> </View> ); }
使用 Expo Router 的启动屏包装器
Expo Router 对 expo-splash-screen 进行了包装,并添加了特殊处理,以确保在导航挂载后以及捕获到意外错误时将其隐藏。只需将导入从 expo-splash-screen 迁移为从 expo-router 导入 SplashScreen 即可。
导航状态观察
如果你正在直接观察导航状态,请迁移到 usePathname、useSegments 和 useGlobalSearchParams hooks。
向嵌套屏幕传递参数
不要使用 嵌套屏幕导航事件,而是使用带限定的 href:
// React Navigation navigation.navigate('Account', { screen: 'Settings', params: { user: 'jane' }, }); // Expo Router router.push({ pathname: '/account/settings', params: { user: 'jane' } });
为深度链接和服务端导航设置初始路由
在 React Navigation 中,你可以使用链接配置的 initialRouteName 属性。在 Expo Router 中,请使用布局设置。
重置导航状态
你可以使用 React Navigation 库中的 reset 动作来重置导航状态。它通过 Expo Router 的 useNavigation hook 分发,以访问 navigation 属性。
在下面的示例中,navigation 属性可通过 useNavigation hook 获取,而 CommonActions.reset 动作来自 @react-navigation/native。reset 动作中指定的对象会用新状态替换现有的导航状态。
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 ( <> {/* ...其余代码 */} <Button title='重置' onPress={handleResetAction} /> </> ); }
迁移 TypeScript 类型
Expo Router 可以自动生成静态类型路由,这将确保你只能导航到有效路由。
其他信息
React Navigation 主题
React Navigation 的导航器 <Stack>、<Drawer> 和 <Tabs> 使用共享的外观提供器。在 React Navigation 中,你可以使用 <NavigationContainer /> 组件为整个应用设置主题。Expo Router 会管理根容器,因此你可以直接使用 ThemeProvider 来设置主题。
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 hook 访问。
React Navigation Elements
@react-navigation/elements 库提供了一组可用于构建导航界面的 UI 元素和辅助工具。这些组件被设计为可组合且可定制。你可以重用该库中的默认功能,或者在其基础上构建你的导航器 UI。
要在 Expo Router 中使用它,你需要安装该库:
- npx expo install @react-navigation/elements要了解该库提供的更多组件和工具,请参阅 Elements 库 文档。