从 React Navigation 迁移

编辑

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


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

介绍

除了 React Navigation 的所有优点外,Expo Router 还支持自动深层链接、类型安全延迟打包Web 的静态渲染以及更多功能。

反向介绍

如果您的应用使用自定义的 getPathFromStategetStateFromPath 组件,可能不适合 Expo Router。如果您使用这些函数来支持 共享路由,那么您应该没问题,因为 Expo Router 内置了对这点的支持。

建议

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

  • 将 React Navigation 的 screen 组件拆分成单独的文件。例如,如果您有 <Stack.Screen component={HomeScreen} />,则确保 HomeScreen 组件位于自己的文件中。
  • 将项目转换为 TypeScript。这将使您更容易发现迁移过程中可能出现的错误。
  • 将相对导入转换为 类型别名。例如,在开始迁移之前,将 ../../components/button.tsx 转换为 @/components/button。这使得在文件系统中移动屏幕时,无需更新相对路径。
  • 避免使用 resetRoot。这用于在运行时“重启”应用程序。这通常被认为是一个不好的实践,您应该重构应用的导航,以便不再需要此操作。
  • 将初始路由重命名为 index。Expo Router 将启动时打开的路由视为匹配 /,而 React Navigation 用户通常将初始路由称为“Home”。

重构搜索参数

将屏幕重构为 使用可序列化的顶级查询参数。我们在 React Navigation 中也推荐这样做。

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

如果您的代码类似于以下内容:

import { useNavigation } from '@react-navigation/native'; const navigation = useNavigation(); navigation.push('Followers', { onPress: profile => { navigation.push('User', { profile }); }, });

请考虑重构,以便该函数可以从“关注者”屏幕访问。在这种情况下,您可以从“关注者”屏幕访问路由并直接推送。

急切加载 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 的支持。

- import { SafeAreaProvider } from 'react-native-safe-area-context'; export default function App() { return ( - <SafeAreaProvider> <MyApp /> - </SafeAreaProvider> ) }

Expo Router 添加 react-native-gesture-handler(自 v3 起),因此如果您使用 Gesture Handler 或 <Drawer /> 布局,您需要自己添加此内容。在 Web 上避免使用此包,因为它添加的很多 JavaScript 经常未使用。

将屏幕复制到应用目录

在您的仓库根目录或根 src 目录创建一个 app 目录。

通过根据 Expo Router 规则的应用 创建文件来布局您应用的结构。使用 kebab-case 和小写字母被认为是路由文件名的最佳实践。

用目录替换导航器,例如:

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

  • 将“主”路由从 Home 重命名为 index,以确保其匹配 / 路径。
  • 将名称转换为小写字母。
  • 将所有屏幕移动到应用目录中的适当文件位置。这可能需要一些实验。
app
_layout.js
(home)
  _layout.js
  index.js
  feed.js
profile.js
settings.js
app/_layout.js
import { Stack } from 'expo-router'; export default function RootLayout() { return ( <Stack> <Stack.Screen name="(home)" options={ { title: 'Home Screen', } } /> </Stack> ); }

选项卡导航器将移动到子目录。

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

使用 Expo Router 钩子

React Navigation v6 及更低版本将 { navigation, route } 属性传递给每个屏幕。这种模式在 React Navigation 中将逐步消失,并且我们从未将其引入到 Expo Router 中。

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

+ import { useRouter } from 'expo-router'; export default function Page({ - navigation }) { - navigation.push('User', { user: 'bacon' }); + const router = useRouter(); + router.push('/users/bacon'); }

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

+ import { useLocalSearchParams } from 'expo-router'; export default function Page({ - route }) { - const user = route?.params?.user; + const { user } = useLocalSearchParams(); }

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

+ import { useNavigation } from 'expo-router'; export default function Page({ + const navigation = useNavigation(); return ( <Button onPress={navigation.navigate('screenName')}> ) })

迁移 Link 组件

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

// React Navigation <Link to="Settings" /> // Expo Router <Link href="/settings" />

React Navigation 用户通常会使用 useLinkProps 钩子创建自定义 Link 组件,以控制子组件。在 Expo Router 中不需要这样做,而是直接使用 asChild 属性。

在导航器之间共享屏幕

在 React Navigation 应用中,通常会在多个导航器之间重用一组路由。这通常与标签一起使用,以确保每个标签都可以推送任意屏幕。

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

当您使用组或共享路由时,您可以通过使用完全合格的路由名称导航到特定标签,例如 /(home)/settings 而不是 /settings

迁移屏幕跟踪事件

您可能根据我们的 React Navigation 屏幕跟踪指南 设置了屏幕跟踪,请根据 Expo Router 屏幕跟踪指南 更新。

根据平台使用特定组件来显示屏幕

有关根据平台切换 UI 的信息,请参阅 平台特定模块 指南。

替换 NavigationContainer

全局的 React Navigation <NavigationContainer /> 在 Expo Router 中完全由其管理。Expo Router 提供系统,以实现与 NavigationContainer 相同的功能,而无需直接使用它。

API 替代

Ref

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

resetRoot​

导航到应用程序的初始路由。例如,如果您的应用从 / 开始(推荐),那么您可以使用此方法将当前路由替换为 /

import { useRouter } from 'expo-router'; function Example() { const router = useRouter(); return ( <Text onPress={() => { // 转到应用程序的初始路由。 router.replace('/'); }}> Reset App </Text> ); }

getRootState

使用 useRootNavigationState()

getCurrentRoute

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

getCurrentOptions

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

addListener

可以迁移以下事件:

state

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

options

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

属性

迁移以下 <NavigationContainer /> 属性:

initialState

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

避免使用此模式,优先使用深层链接(例如,用户打开您的应用到 /profile 而不是从主屏幕),因为这最类似于 Web。如果应用因特定屏幕崩溃,最好避免在应用启动时自动导航回确切的屏幕,因为这可能需要重新安装应用以修复。

onStateChange

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

onReady

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

onUnhandledAction

在 Expo Router 中,操作始终被处理。使用 动态路由404 屏幕 替代 onUnhandledAction

linking

linking 属性是基于 app 目录中的文件自动构建的。

fallback

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

theme

在 React Navigation 中,您使用 <NavigationContainer /> 组件为整个应用设置主题。Expo Router 为您管理根容器,因此相反,您应该直接使用 ThemeProvider 设置主题。

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 钩子访问。

children

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

independent

Expo Router 不支持 independent 容器。这是因为路由器负责管理单个 <NavigationContainer />。任何其他容器将不会由 Expo Router 自动管理。

documentTitle

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

ref

请改用 useNavigationContainerRef() 钩子。

重写自定义导航器

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

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

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

要重写,请使用 Navigator 组件,该组件将包装来自 React Navigation 的 useNavigationBuilder 钩子。

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

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

  • Navigator.useContext:访问 React Navigation 的 statenavigationdescriptorsrouter 用于自定义导航器。
  • 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="/">Home</Link> <Link href="/profile" style={[pathname === '/profile' && { color: 'blue' }]}> Profile </Link> <Link href="/settings">Settings</Link> </View> ); }

使用 Expo Router 的启动屏幕包装器

Expo Router 包装了 expo-splash-screen 并添加了特殊处理,以确保在导航挂载后以及每当捕获到意外错误时隐藏它。只需将导入 expo-splash-screen 迁移到导入来自 expo-routerSplashScreen

观察导航状态

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

将参数传递给嵌套屏幕

而不是使用 嵌套屏幕导航事件,请使用合格的 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 钩子派发的 navigation 属性。

在以下示例中,navigation 属性可以从 useNavigation 钩子和来自 @react-navigation/nativeCommonActions.reset 操作访问。reset 操作中指定的对象将用新对象替换现有的导航状态。

app/screen.js
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} /> </> ); }

迁移 TypeScript 类型

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

附加信息

React Navigation 主题

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

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

您可以在应用的任何层使用此技术设置特定布局的主题。当前主题可以通过 useTheme 钩子访问。

React Navigation 元素

@react-navigation/elements 库提供了一组 UI 元素和帮助程序,可以用来构建导航 UI。这些组件旨在可组合和可定制。您可以重用库中的默认功能或在其基础上构建您自己的导航器 UI。

要与 Expo Router 一起使用,您需要安装该库:

Terminal
npm install @react-navigation/elements
Terminal
yarn add @react-navigation/elements

要了解有关该库提供的组件和工具的更多信息,请参见 Elements library 文档。