从 React Navigation 迁移
编辑页面
了解如何将使用 React Navigation 的项目迁移到 Expo Router。
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
本指南适用于 SDK 56 及更高版本。在 SDK 56 中,Expo Router 停止接受来自@react-navigation/*.的应用代码导入。它们现在来自expo-router/\*入口点。如果你正在将现有的 Expo Router 应用从 SDK 55 或更早版本升级,请按照 SDK 55 到 56 迁移指南 进行操作。下面的示例已经使用了 SDK 56 及更高版本的导入路径。
前言
除了 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: '主页屏幕', }} /> </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: '主页屏幕', } } /> </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 替代项
引用
不应直接访问 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(() => {}, [...]) 使用以观察变化。
属性
迁移以下 <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 'expo-router/react-navigation'; import { Slot } from 'expo-router'; export default function RootLayout() { return ( <ThemeProvider value={DarkTheme}> <Slot /> </ThemeProvider> ); }
你可以在应用的任何层使用此技术来为特定布局设置主题。当前主题可以通过 expo-router/react-navigation 中的 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 'expo-router/react-navigation'; 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 动作来自 expo-router/react-navigation。在 reset 动作中指定的对象会用新的导航状态替换现有的导航状态。
import { useNavigation } from 'expo-router' import { CommonActions } from 'expo-router/react-navigation' 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 'expo-router/react-navigation'; import { Slot } from 'expo-router'; export default function RootLayout() { return ( <ThemeProvider value={DarkTheme}> <Slot /> </ThemeProvider> ); }
你可以在应用的任何层级使用此技术,为特定布局设置主题。当前主题可以通过 expo-router/react-navigation 中的 useTheme 钩子访问。
React Navigation 元素
React Navigation Elements 库提供了一组 UI 元素和辅助工具,可用于构建导航 UI。这些组件设计为可组合且可定制的。你可以复用该库中的默认功能,或在此基础上构建你自己的导航器 UI。
在 SDK 56 及更高版本中,该库会从 expo-router/react-navigation 重新导出,因此无需单独安装包:
import { Header, HeaderBackButton } from 'expo-router/react-navigation';
要了解该库提供的更多组件和工具,请参阅 Elements 库 文档。