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 导入内容、访问导航状态,或实现自定义路由器和导航器的应用则需要仔细检查。
使用以下清单,找到适用于你应用的部分:
- 对于常见的应用迁移,优先使用
useRouter,将嵌套导航替换为 href,并将initialRouteName移至unstable_settings。 - 对于 Web 应用,检查异步路由默认设置;如果使用
web.output: "server",还需检查服务器渲染更改。 - 对于高级集成,请更新分发操作、读取或持久化导航状态的代码,以及实现自定义路由器和自定义导航器的代码。
- 如果应用从
expo-router/react-navigation入口点导入内容,请检查已移除的expo-router/react-navigationAPI。
本指南涵盖最可能需要更新应用的更改。有关 SDK 58 更改的完整列表,请参阅
expo-router更新日志。
常见迁移
检查服务器渲染行为
在 SDK 58 中,web.output: "server" 会在每次请求时渲染 HTML 页面,而不是在导出期间预渲染这些页面。
若要保持 SDK 57 及更早版本中 web.output: "server" 的行为,请将 web.output 设置为 "static",并在应用配置的 expo-router 配置插件中启用 apiRoutes: true:
这会将预渲染的 HTML 与 API 路由结合使用,并且仍需要已部署的服务器。若要在每次请求时渲染 HTML,请保留 web.output: "server",并遵循服务器渲染指南。
检查异步路由默认设置
在 SDK 58 中,Web 上的异步路由在开发和生产环境中均默认启用。原生平台的默认设置不变,原生生产构建仍会同步加载路由。
异步路由使用默认加载回退界面,而不是布局中自定义的 SuspenseFallback 导出。如果 Web 应用依赖自定义回退界面,请在应用配置的 expo-router 配置插件中禁用 Web 上的异步路由:
这会恢复同步路由加载,并支持在 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:
更多示例请参阅导航到嵌套导航器中的屏幕。
在命令式导航期间,祖先路由参数也不再复制到后代路由中。如果应用依赖后代路由从其父路由接收参数,请将该值包含在目标 href 中。这样,命令式导航就与通过深层链接或冷启动打开相同 href 的行为保持一致。
将 initialRouteName 移至 unstable_settings
initialRouteName 导航器属性已移除。若要在通过深层链接进入堆栈时添加返回目的地,请从该堆栈的布局中导出 unstable_settings.anchor:
不要使用
anchor选择应用的初始屏幕。该屏幕由启动 URL 决定。对于默认的/URL,请使用 index.tsx 路由。详情请参阅核心概念。
Expo Router 现在会根据 URL 构建完整的初始状态。例如,嵌套在标签页中的堆栈会读取其自身 _layout.tsx 文件中的设置。在命令式导航期间,如果需要将锚点路由插入目标路由下方,请传入 { withAnchor: true }。
有关初始路由和锚点行为,请参阅路由器设置。
替换 redirect 和 initialParams
布局 Screen 组件不再接受 redirect 或 initialParams。
请在路由文件中渲染 Redirect,而不是在屏幕上配置 redirect:
若要进行访问控制,请使用带有 redirectTo 的受保护路由:
请在屏幕读取参数的位置设置默认值,以替换 initialParams:
声明标签页和抽屉屏幕
JavaScript 标签页、顶部标签页、抽屉、无头标签页和原生标签页现在只显示布局中声明的屏幕。请声明所有应在导航器 UI 中显示的路由:
更新受保护路由
大多数使用受保护路由的应用无需更改。在 SDK 58 中,受保护路由仍会注册在其导航器中。守卫条件未通过的路由会渲染重定向,而不是从路由树中移除。
仅当你想控制重定向目标时,才在导航器的 Protected 组件上设置 redirectTo。如果未设置,Expo Router 会使用导航器中可访问的锚点或初始路由,然后使用第一个可访问路由。
有关守卫模式,请参阅受保护路由。
替换 Web 模态框
实验性的 Web 模态框实现已移除。请遵循构建自定义 Web 模态框,通过自定义导航器渲染模态覆盖层。自定义实现可以从 expo-router 导入 NativeStackView,用于原生堆栈视图。
替换 freezeOnBlur
freezeOnBlur 选项在 SDK 58 中不生效。请从屏幕配置中移除它。若要在屏幕失去焦点后清理副作用的同时保留状态,请在导航器或已声明的屏幕上设置 activityEnabled。值为 1 时,只要另一个屏幕获得焦点,该屏幕内容就会隐藏:
当 activityEnabled 为 true 时,如果某个屏幕上方有两个屏幕,堆栈就会隐藏该屏幕的内容。标签页和抽屉会在屏幕失去焦点时立即隐藏其内容。堆栈导航器和屏幕也接受正数,用于覆盖该阈值。
若只需隐藏屏幕的一部分,请改为将该部分内容包裹在 NavigationAwareActivity 中。其 hideWhenNestedAtLevel 属性使用相同的阈值行为,默认值为 2。
安装可选原生依赖项
在 SDK 58 中,expo-symbols 和 @expo/ui 成为 Expo Router 的可选对等依赖项。请仅安装应用所用 API 需要的依赖项:
安装任一依赖项后,请重新构建使用受影响原生 API 的开发构建。
防止屏幕被移除
只有当现有的 usePreventRemove 调用会重复或替换被阻止的操作时,才需要更改。若要继续执行完全相同的被阻止操作,请将阻止条件设为 false,并调用回调的 repeat 函数。
使用来自 expo-router 的 usePreventRemove,在屏幕有未保存数据时阻止其被移除:
若要导航到被阻止目标以外的位置,请保留 usePreventRemove 返回的 disablePrevention 函数。将阻止条件设为 false,调用 disablePrevention(),然后进行导航。阻止仍处于启用状态时,不要在原始的 removePrevented 监听器中分发操作,因为该操作可能会再次被阻止。
调用
disablePrevention()时,请务必更新传递给usePreventRemove的布尔值。如果该值仍为true,Hook 会发出警告,并且在布尔值发生变化之前不会重新启用阻止。
高级迁移
更新 navigation.dispatch
navigation.dispatch 和导航辅助操作现在会排队,直到当前 React 提交完成。只有当集成必须立即应用操作时,才使用 navigation.dispatchSync(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 会构建完整的初始状态,而包装器会保留基础路由器的行为和状态不变量。
更新路由器接口
将现有自定义路由器中与基础路由器不同的部分移入扩展中。扩展未覆盖的成员将从基础路由器继承:
基础堆栈和标签页路由器会处理 PUSH。如果扩展覆盖了 getStateForAction,请将它未处理的操作委托给提供的 baseRouter。
返回受影响的路由键
现在,getStateForAction 会返回下一个状态以及受操作影响的路由键:
请将 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 保持一致。
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:
从 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 入口点中。