This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

将 Expo Router 从 SDK 57 迁移到 SDK 58

编辑页面

了解如何将 Expo Router 应用从 SDK 57 迁移到 SDK 58。


SDK 58 更改了 Expo Router 构建导航状态以及与导航器集成的方式。使用 Stack、Link 和 useRouter 的应用可能需要进行一些更改。从 expo-router/react-navigation 导入内容、访问导航状态,或实现自定义路由器和导航器的应用则需要仔细检查。

使用以下清单,找到适用于你应用的部分:

常见迁移

检查服务器渲染行为

在 SDK 58 中,web.output: "server" 会在每次请求时渲染 HTML 页面,而不是在导出期间预渲染这些页面。

若要保持 SDK 57 及更早版本中 web.output: "server" 的行为,请将 web.output 设置为 "static",并在应用配置的 expo-router 配置插件中启用 apiRoutes: true:

app.json
{ "expo": { "web": { "output": "static" }, "plugins": [["expo-router", { "apiRoutes": true }]] } }

这会将预渲染的 HTML 与 API 路由结合使用,并且仍需要已部署的服务器。若要在每次请求时渲染 HTML,请保留 web.output: "server",并遵循服务器渲染指南。

检查异步路由默认设置

在 SDK 58 中,Web 上的异步路由在开发和生产环境中均默认启用。原生平台的默认设置不变,原生生产构建仍会同步加载路由。

异步路由使用默认加载回退界面,而不是布局中自定义的 SuspenseFallback 导出。如果 Web 应用依赖自定义回退界面,请在应用配置的 expo-router 配置插件中禁用 Web 上的异步路由:

app.json
{ "expo": { "plugins": [["expo-router", { "asyncRoutes": { "web": false } }]] } }

这会恢复同步路由加载,并支持在 Web 上使用自定义加载回退界面。现有的显式 asyncRoutes 设置仍优先于默认设置。有关配置选项和限制,请参阅异步路由。

升级或更改 asyncRoutes 后,请使用 expo start --clear 或 expo export --clear 清除 Metro 缓存。

在组件中优先使用 useRouter

从组件进行基于 href 的导航时,优先使用 useRouter() 返回的路由器,而不是 useNavigation().navigate() 或模块级的 router。该 Hook 会绑定到渲染此组件的 Expo Router 根节点。

模块级的 router 仍可使用,但在首次渲染路由器之前会抛出错误,并且无法区分多个路由器根节点。当需要导航器专属 API(例如事件、选项或操作分发)时,请继续使用 useNavigation。

useRouter() 包含常见的导航方法,例如 push、navigate、replace、back、dismiss、dismissTo、dismissAll、canGoBack、canDismiss、setParams 和 prefetch。如果缺少所需的导航方法,请提交问题。

使用完整 href 进行导航

在 SDK 57 中,React Navigation 可以将 screen、params 和 initial 解释为构建嵌套状态的指令。在 SDK 58 中,它们是普通的用户参数。请改为导航到完整 href:

app/profile.tsx
1import { useNavigation } from 'expo-router';
1import { useRouter } from 'expo-router';
22
3const navigation = useNavigation();
3const router = useRouter();
44
5navigation.navigate('(tabs)', {
6 screen: 'feed',
5router.push({
6 pathname: '/(tabs)/feed/[id]',
77params: { id: '42' },
88});

更多示例请参阅导航到嵌套导航器中的屏幕。

在命令式导航期间,祖先路由参数也不再复制到后代路由中。如果应用依赖后代路由从其父路由接收参数,请将该值包含在目标 href 中。这样,命令式导航就与通过深层链接或冷启动打开相同 href 的行为保持一致。

将 initialRouteName 移至 unstable_settings

initialRouteName 导航器属性已移除。若要在通过深层链接进入堆栈时添加返回目的地,请从该堆栈的布局中导出 unstable_settings.anchor:

app/(tabs)/_layout.tsx
11import { Stack } from 'expo-router';
22
3export const unstable_settings = {
4 anchor: 'index',
5};
6
37export default function FeedLayout() {
4 return <Stack initialRouteName="index" />;
8 return <Stack />;
59}

Expo Router 现在会根据 URL 构建完整的初始状态。例如,嵌套在标签页中的堆栈会读取其自身 _layout.tsx 文件中的设置。在命令式导航期间,如果需要将锚点路由插入目标路由下方,请传入 { withAnchor: true }。

有关初始路由和锚点行为,请参阅路由器设置。

替换 redirect 和 initialParams

布局 Screen 组件不再接受 redirect 或 initialParams。

请在路由文件中渲染 Redirect,而不是在屏幕上配置 redirect:

app/legacy.tsx
import { Redirect } from 'expo-router'; export default function LegacyRoute() { return <Redirect href="/replacement" />; }

若要进行访问控制,请使用带有 redirectTo 的受保护路由:

app/_layout.tsx
import { Stack } from 'expo-router'; import { useAuth } from '../context/auth'; export default function RootLayout() { const isSignedIn = useAuth(); return ( <Stack> <Stack.Protected guard={isSignedIn} redirectTo="/sign-in"> <Stack.Screen name="account" /> </Stack.Protected> <Stack.Screen name="sign-in" /> </Stack> ); }

请在屏幕读取参数的位置设置默认值,以替换 initialParams:

app/feed.tsx
import { useLocalSearchParams } from 'expo-router'; export default function Feed() { const { sort = 'latest' } = useLocalSearchParams<{ sort?: string }>(); // ... }

声明标签页和抽屉屏幕

JavaScript 标签页、顶部标签页、抽屉、无头标签页和原生标签页现在只显示布局中声明的屏幕。请声明所有应在导航器 UI 中显示的路由:

app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'; export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="index" /> <Tabs.Screen name="feed" /> <Tabs.Screen name="hidden" options={{ href: null }} /> </Tabs> ); }

更新受保护路由

大多数使用受保护路由的应用无需更改。在 SDK 58 中,受保护路由仍会注册在其导航器中。守卫条件未通过的路由会渲染重定向,而不是从路由树中移除。

仅当你想控制重定向目标时,才在导航器的 Protected 组件上设置 redirectTo。如果未设置,Expo Router 会使用导航器中可访问的锚点或初始路由,然后使用第一个可访问路由。

有关守卫模式,请参阅受保护路由。

替换 Web 模态框

实验性的 Web 模态框实现已移除。请遵循构建自定义 Web 模态框,通过自定义导航器渲染模态覆盖层。自定义实现可以从 expo-router 导入 NativeStackView,用于原生堆栈视图。

替换 freezeOnBlur

freezeOnBlur 选项在 SDK 58 中不生效。请从屏幕配置中移除它。若要在屏幕失去焦点后清理副作用的同时保留状态,请在导航器或已声明的屏幕上设置 activityEnabled。值为 1 时,只要另一个屏幕获得焦点,该屏幕内容就会隐藏:

app/_layout.tsx
import { Stack } from 'expo-router'; export default function RootLayout() { return ( <Stack> {/* 使用堆栈默认设置,并在其上方有两个屏幕时隐藏内容。 */} <Stack.Screen name="feed" activityEnabled /> {/* 另一个屏幕获得焦点后立即隐藏内容。 */} <Stack.Screen name="account" activityEnabled={1} /> </Stack> ); }

当 activityEnabled 为 true 时,如果某个屏幕上方有两个屏幕,堆栈就会隐藏该屏幕的内容。标签页和抽屉会在屏幕失去焦点时立即隐藏其内容。堆栈导航器和屏幕也接受正数,用于覆盖该阈值。

若只需隐藏屏幕的一部分,请改为将该部分内容包裹在 NavigationAwareActivity 中。其 hideWhenNestedAtLevel 属性使用相同的阈值行为,默认值为 2。

安装可选原生依赖项

在 SDK 58 中,expo-symbols 和 @expo/ui 成为 Expo Router 的可选对等依赖项。请仅安装应用所用 API 需要的依赖项:

Terminal
# 原生标签页中的 Android md 图标
- npx expo install expo-symbols
# Android Stack.Toolbar
- npx expo install @expo/ui
# 原生标签页中的 Android md 图标
- yarn expo install expo-symbols
# Android Stack.Toolbar
- yarn expo install @expo/ui
# 原生标签页中的 Android md 图标
- pnpm expo install expo-symbols
# Android Stack.Toolbar
- pnpm expo install @expo/ui
# 原生标签页中的 Android md 图标
- bun expo install expo-symbols
# Android Stack.Toolbar
- bun expo install @expo/ui

安装任一依赖项后,请重新构建使用受影响原生 API 的开发构建。

防止屏幕被移除

只有当现有的 usePreventRemove 调用会重复或替换被阻止的操作时,才需要更改。若要继续执行完全相同的被阻止操作,请将阻止条件设为 false,并调用回调的 repeat 函数。

使用来自 expo-router 的 usePreventRemove,在屏幕有未保存数据时阻止其被移除:

app/edit-profile.tsx
import { usePreventRemove } from 'expo-router'; import { useState } from 'react'; import { Alert, Button } from 'react-native'; export default function EditProfile() { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); usePreventRemove(hasUnsavedChanges, ({ repeat }) => { Alert.alert('放弃更改?', '你的更改尚未保存。', [ { text: '继续编辑', style: 'cancel' }, { text: '放弃', style: 'destructive', onPress: () => { setHasUnsavedChanges(false); repeat(); }, }, ]); }); return <Button title="保存" onPress={() => setHasUnsavedChanges(false)} />; }

若要导航到被阻止目标以外的位置,请保留 usePreventRemove 返回的 disablePrevention 函数。将阻止条件设为 false,调用 disablePrevention(),然后进行导航。阻止仍处于启用状态时,不要在原始的 removePrevented 监听器中分发操作,因为该操作可能会再次被阻止。

高级迁移

更新 navigation.dispatch

navigation.dispatch 和导航辅助操作现在会排队,直到当前 React 提交完成。只有当集成必须立即应用操作时,才使用 navigation.dispatchSync(action)。

不再支持分发函数。请先读取状态,计算操作,然后分发操作对象:

navigation.ts
1navigation.dispatch(state =>
2 CommonActions.reset({
3 ...state,
4 index: 0,
5 routes: [state.routes[0]],
6 })
7);
1const state = navigation.getState();
2const action = CommonActions.reset({
3 ...state,
4 index: 0,
5 routes: [state.routes[0]],
6});
7
8navigation.dispatch(action);

读取和延迟分发不是原子操作。如果二者之间不能发生其他导航操作,请有意识地使用 dispatchSync(action)。

替换 navigationKey

布局 Screen 和 Group 组件不再接受 navigationKey。没有直接的替代方案。请根据使用该键的原因,改用路由和布局标识、受保护路由或显式 href 导航。

替换 beforeRemove 和 __unsafe_action__

beforeRemove 和 __unsafe_action__ 事件已移除。使用 removed 观察已完成的移除,使用 removePrevented 观察被 usePreventRemove 阻止的操作。

removed 事件会在路由卸载后触发。请延迟清理其监听器,以便监听器能够接收该事件:

useEffect(() => { const unsubscribe = navigation.addListener('removed', event => { logRemoval(event.data.action); }); return () => queueMicrotask(unsubscribe); }, [navigation]);

不要在此监听器中更新已移除屏幕的状态,因为该屏幕已经卸载。

将 useNavigation 移入导航器内部

现在,在导航器外部调用 useNavigation 会抛出错误。如果你手动渲染 ExpoRoot,这也包括由其 wrapper 属性渲染的组件。请将 Hook 移至导航树内部渲染的路由或布局中。

读取导航状态

导航状态属于实现细节。需要 URL 或路由信息时,优先使用 usePathname、useSegments 和 useGlobalSearchParams。

处理可选的 type 和 history

对于自定义路由器,导航状态的 type 是可选的。在标签页和抽屉状态中,history 是可选的。读取它们之前请添加回退值:

const history = state.history ?? []; if (state.type === 'tab') { // 处理标签页专属状态。 }

从 routes 读取堆栈预加载路由

StackNavigationState.preloadedRoutes 已移除。预加载路由会添加到 state.routes 中焦点索引之后:

const activeRoutes = state.routes.slice(0, state.index + 1); const preloadedRoutes = state.routes.slice(state.index + 1);

使用 routeNames 获取标签页顺序

TabNavigationState.preloadedRouteKeys 已移除。预加载标签页是 state.routes 中带有 isPreloaded: true 的非焦点条目,而延迟加载路由可能不会出现在该数组中。需要遍历声明的标签页顺序时,请遍历 state.routeNames:

for (const routeName of state.routeNames) { const route = state.routes.find(route => route.name === routeName); // 延迟加载的路由尚未创建时,`route` 为 undefined。 }

移除抽屉 default

DrawerNavigationState.default 已移除。在组件中,请使用来自 expo-router/drawer 的 useDrawerStatus 读取抽屉是打开还是关闭。getDrawerStatusFromState 已弃用,现在要求将路由器的默认状态作为第二个参数。

在完整状态中包含 routeKeySeq

持久化的初始状态必须完整。每个嵌套状态都需要包含状态键、路由键、routeKeySeq、routeNames、index 和 stale: false。Expo Router 收到不完整的持久化初始状态时会抛出错误。

自定义路由器扩展返回的导航状态仍包含 routeKeySeq,但扩展不会直接更新它。请使用 ...state 保留当前状态,并使用 extendRouter 提供的 nextKey 函数。包装器会将更新后的序列写入返回的状态。

CommonActions.reset 也要求状态包含 stale: false,但路由器会生成省略的路由键,并将省略的 routeKeySeq 填充为当前状态中的值。

重置状态时,请使用 ...state 保留当前导航器的键、路由名称和键序列。请参阅更新 navigation.dispatch中的示例。

持久化状态的每个嵌套层级都必须保留有效的键和路由名称。

更新自定义路由器

在 SDK 58 中,请使用 extendRouter 或 extendRouterActions 扩展 StackRouter 或 TabRouter 来创建自定义路由器。Expo Router 会构建完整的初始状态,而包装器会保留基础路由器的行为和状态不变量。

更新路由器接口

将现有自定义路由器中与基础路由器不同的部分移入扩展中。扩展未覆盖的成员将从基础路由器继承:

SDK 57 APISDK 58 迁移
getInitialState移除。Expo Router 会构建初始状态。
getRehydratedState移除。持久化数据必须提供完整状态。
getStateForRouteNamesChange继承基础行为,或在扩展中处理 ROUTE_NAMES_CHANGED。
routeParamList从 RouterConfigOptions 中移除。在路由代码中设置参数默认值。
RouterActionOptions在 getStateForAction 中使用 RouterConfigOptions。
其他路由器成员除非扩展会更改其行为,否则从基础路由器继承。

基础堆栈和标签页路由器会处理 PUSH。如果扩展覆盖了 getStateForAction,请将它未处理的操作委托给提供的 baseRouter。

返回受影响的路由键

现在,getStateForAction 会返回下一个状态以及受操作影响的路由键:

custom-router.ts
1return nextState;
1// `affectedRoute` 由此操作的处理逻辑选出。
2return {
3 state: nextState,
4 affectedRouteKey: affectedRoute?.key,
5};

请将 affectedRouteKey 设置为此操作选中或更改的路由;它不一定是焦点路由。路由器无法处理某个操作时,请返回 null。

扩展内置路由器

extendRouter 会接收基础路由器和 nextKey 函数。nextKey 会生成确定性的路由键,包装器会将更新后的 routeKeySeq 写入返回的状态:

import { attachRouteState, extendRouter, StackRouter, type CommonNavigationAction, type StackActionType, } from 'expo-router'; type CustomAction = { type: 'CUSTOM-ACTION'; payload: { name: string; params?: object }; }; const CustomStackRouter = extendRouter(StackRouter, ({ baseRouter, nextKey }) => ({ getStateForAction( state, action: CommonNavigationAction | StackActionType | CustomAction, config ) { if (action.type === 'CUSTOM-ACTION') { if (!state.routeNames.includes(action.payload.name)) { return null; } const route = attachRouteState( { key: nextKey(action.payload.name), name: action.payload.name, params: action.payload.params, }, action ); const activeRoutes = state.routes.slice(0, state.index + 1); const preloadedRoutes = state.routes.slice(state.index + 1); return { state: { ...state, index: activeRoutes.length, routes: [...activeRoutes, route, ...preloadedRoutes], }, affectedRouteKey: route.key, }; } return baseRouter.getStateForAction(state, action, config); }, }));

如果只需自定义 getStateForAction,请使用 extendRouterActions。其 reducer 返回 undefined 时会委托处理该操作。使用 extendRouter 覆盖 getStateForAction 时,请像上面一样将未处理的操作委托给 baseRouter。

在适当时将路由器类型设为可选

extendRouter 会继承基础路由器的 type。如果扩展更改后的状态需要不同的类型,请在包装器的选项中传入该类型。状态类型为可选的扩展可以省略它。

更新自定义导航器

对于新的自定义集成,请在应用中使用 createStandardRouterNavigator,或在可复用库中使用 integrateWithRouter。这些 API 使用 standard-navigation 契约,使导航器状态、描述符、操作和事件与 Expo Router 保持一致。

src/expo-router.ts
1import { withLayoutContext } from 'expo-router';
2import { createNavigator } from './navigator';
1import { createStandardRouterNavigator, TabRouter } from 'expo-router';
2import { TabNavigatorContent } from './navigator';
33
4export const Tabs = withLayoutContext(createNavigator().Navigator);
4export const Tabs = createStandardRouterNavigator(TabNavigatorContent, TabRouter);

withLayoutContext 仍支持现有的 React Navigation 导航器。如果你的集成使用了它以前的第三个参数 useOnlyUserDefinedScreens,请将其移除。文件系统路由仍会注册。需要在导航器 UI 中区分布局声明的路由和文件系统路由时,请使用描述符 routeSource。

此示例假设 TabNavigatorContent 已从 React Navigation 导航器工厂转换为接受 NavigatorContentProps 的组件。库作者应改用 standard-navigation 中的 createStandardNavigator 创建与框架无关的导航器,然后将该导航器传给 integrateWithRouter。

expo-router/js-stack 中的 createStackNavigator 导出已移除。这不会影响 expo-router/native-stack 中仍可用的 createNativeStackNavigator。对于新的堆栈集成,建议使用 createStandardRouterNavigator 创建标准导航器,或将其与匹配的 props 辅助函数一起传给 integrateWithRouter:

导航器createProps 辅助函数
自定义堆栈createBaseStackProps
Expo Router JavaScript 堆栈createJSStackProps
Expo Router 原生堆栈createNativeStackProps
自定义标签栏createBaseTabProps
Expo Router JavaScript 标签栏createJSTabsProps
Expo Router JavaScript 顶部标签栏createJSTopTabsProps
Expo Router 原生标签栏createNativeTabsProps

从 expo-router 导入基础和原生堆栈辅助函数。从对应的 expo-router/js-stack、expo-router/js-tabs、expo-router/js-top-tabs 或 expo-router/native-tabs 入口点导入特定导航器的辅助函数。

对于尚无活动状态路由的声明路由,descriptor.route 上的 key 可能为 undefined。请更新自定义导航器代码以处理这种情况。

有关标准导航器概念,请参阅自定义导航器。

检查已移除的 expo-router/react-navigation 导出

以下兼容性 API 已在 SDK 58 中移除或更改。仍受支持的导入列在 expo-router/react-navigation 入口点中。

已移除的 API替代方案变更
UNSTABLE_UnhandledLinkingContext没有应用级替代方案。Expo Router 负责处理未处理的链接。#49616
BaseNavigationContainer、NavigationContainer由 Expo Router 或 ExpoRoot 管理导航容器。#49587、#48760
根 options 事件、DocumentTitleOptions、documentTitle使用 Expo Router Head 或 <title> 元素设置 Web 元数据。#49590
容器 props 上的 onStateChange优先使用 Expo Router 状态钩子,或监听导航 ref 的 state 事件。#49588
NavigationIndependentTree、useNavigationIndependentTree对于隔离的嵌入式导航树,请使用来自 @react-navigation/native 的 NavigationContainer。#49172
React Navigation Link、LinkProps、useLinkProps使用来自 expo-router 的 Link 和 LinkProps,并提供 href。#48895
navigateDeprecated、navigationInChildEnabled使用 useRouter 导航到完整的 href。#49102
NavigatorScreenParams、getActionFromState、LinkingOptions.getActionFromState使用完整的 href,并由 Expo Router 解析导航状态。#49297
导航容器 ref 上的 resetRoot使用 router.replace,或使用完整状态分发 CommonActions.reset。#49297
beforeRemove、__unsafe_action__使用 usePreventRemove、removePrevented 和 removed。#49408
PreventRemoveContext、usePreventRemoveContext、PreventRemoveProvider使用 usePreventRemove。Expo Router 负责管理 provider。#49408、#48347
Router.getInitialState、Router.getRehydratedState由 Expo Router 构建初始状态,并由自定义路由器返回完整状态。#48783、#49297
Router.getStateForRouteNamesChange在 getStateForAction 中处理 ROUTE_NAMES_CHANGED。#48479
RouterActionOptions、RouterConfigOptions.routeParamList使用不带 routeParamList 的 RouterConfigOptions。#48783
DrawerNavigationState.default使用 useDrawerStatus,或向已弃用的 getDrawerStatusFromState 传入默认状态。#48750
静态导航 API 和类型使用 Expo Router 文件路由和布局。#48071
LinkingOptions.enabled移除该选项。链接由 Expo Router 负责处理。#49103
UNSTABLE_routeNamesChangeBehavior使用受保护路由重定向和显式 href 导航。#47985
useOnlyUserDefinedScreens移除该选项。所有文件系统路由仍会注册。#47983