iOS端expo-router导航失效:从register模态页跳转profile失败
使用expo(51.0.21)和expo-router(3.5.18)开发应用时,遇到iOS端导航异常:从register模态页面通过useRouter的navigate方法跳转至profile页面完全失效,但Android端可正常工作。
相关代码
_layout.tsx
const RootLayout = () => { const router = useRouter(); const { user, isLoaded, isSignedIn } = useUser(); const { os } = usePlatform(); const { save } = useMeStore(); const { settings } = useSettingsStore(); const { me } = useMeStore(); const createUserOrFailMutation = useMutation( api.api.user.findUserOrCreateOne ); React.useEffect(() => { if (isLoaded && !isSignedIn) { router.replace("/login"); } }, [isLoaded, isSignedIn]); React.useEffect(() => { if (!isSignedIn) { save(null); } if (!!user) { const me = { firstName: user.firstName, lastName: user.lastName, id: user.id, createdAt: user.createdAt, updatedAt: user.updatedAt, imageUrl: user.imageUrl, lastLoginAt: user.lastSignInAt, email: user.emailAddresses[0].emailAddress, }; save(me); } else { save(null); } }, [user, isSignedIn]); // Try to save a user to convex if not exists React.useEffect(() => { if (!!me) { createUserOrFailMutation({ email: me.email, firstName: me.firstName || "", id: me.id, image: me.imageUrl, lastName: me.lastName || "", }); } }, [me]); return ( <Stack> <Stack.Screen options={{ presentation: os === "ios" ? "modal" : "fullScreenModal", headerTitle: "Sign In", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerLeft: ({}) => ( <TouchableOpacity style={{ marginRight: 20, }} activeOpacity={0.7} onPress={async () => { if (settings.haptics) { await onImpact(); } router.replace("/"); }} > <Ionicons name="close-outline" size={30} color={COLORS.black} /> </TouchableOpacity> ), headerStyle: { backgroundColor: COLORS.white }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, }} name="(modals)/login" /> <Stack.Screen options={{ presentation: os === "ios" ? "modal" : "fullScreenModal", headerTitle: "Forgot Password", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerLeft: ({}) => ( <TouchableOpacity style={{ marginRight: 20, }} activeOpacity={0.7} onPress={async () => { if (settings.haptics) { await onImpact(); } router.back(); }} > <Ionicons name="close-outline" size={30} color={COLORS.black} /> </TouchableOpacity> ), headerStyle: { backgroundColor: COLORS.white }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, }} name="(modals)/forgot_password" /> <Stack.Screen options={{ presentation: os === "ios" ? "modal" : "fullScreenModal", headerTitle: "Sign Up", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerLeft: ({}) => ( <TouchableOpacity style={{ marginRight: 20, flexDirection: "row", gap: 10, alignItems: "center", }} activeOpacity={0.7} onPress={async () => { if (settings.haptics) { await onImpact(); } router.back(); }} > <Ionicons name="chevron-back" size={30} color={COLORS.green} /> <Typography variant="h5" style={{ color: COLORS.green, fontSize: 20, }} > Sign In </Typography> </TouchableOpacity> ), headerStyle: { backgroundColor: COLORS.white }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, }} name="(modals)/register" /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="profile" options={{ headerShown: true, title: "Profile", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, headerBackVisible: false, }} /> <Stack.Screen name="verify" options={{ headerShown: true, title: "Verify Email", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, headerBackVisible: false, headerLeft: ({}) => ( <TouchableOpacity style={{ marginRight: 20, flexDirection: "row", gap: 10, alignItems: "center", }} activeOpacity={0.7} onPress={async () => { if (settings.haptics) { await onImpact(); } router.back(); }} > <Ionicons name="chevron-back" size={30} color={COLORS.green} /> <Typography variant="h5" style={{ color: COLORS.green, fontSize: 20, }} > Sign Up </Typography> </TouchableOpacity> ), }} /> <Stack.Screen name="reset_password" options={{ headerShown: true, title: "Update Password", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, headerBackVisible: false, headerLeft: ({}) => ( <TouchableOpacity style={{ marginRight: 20, flexDirection: "row", gap: 10, alignItems: "center", }} activeOpacity={0.7} onPress={async () => { if (settings.haptics) { await onImpact(); } router.back(); }} > <Ionicons name="chevron-back" size={30} color={COLORS.green} /> <Typography variant="h5" style={{ color: COLORS.green, fontSize: 20, }} > Forgot Pwd </Typography> </TouchableOpacity> ), }} /> <Stack.Screen name="+not-found" /> </Stack> ); };
跳转逻辑代码
const router = useRouter(); <Button title="Hello" onPress={() => router.navigate({ pathname: "/profile", params: { email_address: state.email, }, }) } />
原因分析
在Expo Router的iOS实现中,设置presentation: "modal"的页面会被放入独立的模态导航栈中,而profile页面属于根导航栈(非模态栈)。当你在模态页面调用router.navigate("/profile")时,iOS会尝试在当前的模态栈中寻找并添加profile页面,但你的根布局里profile并没有注册在模态栈中,导致跳转无响应。
而Android端使用的是fullScreenModal,它的模态实现更接近全屏替换,和根栈的导航逻辑兼容性更强,因此能正常跳转到profile页面。
另外需排查是否有路由守卫干扰:注册完成后如果isSignedIn状态未正确更新,根布局中的useEffect可能触发router.replace("/login")覆盖跳转操作,但因Android正常,这种可能性较低,核心问题还是模态栈与根栈的导航隔离。
解决方案
方案1:关闭模态后再跳转(推荐)
先关闭当前模态页面,回到根栈后再导航到profile,符合iOS导航规范:
<Button title="Hello" onPress={() => { // 关闭当前模态页面 router.dismiss(); // 导航到profile页面 router.navigate({ pathname: "/profile", params: { email_address: state.email, }, }); }} />
方案2:修改Profile页面的导航栈归属
如果需要在模态栈中直接打开profile,可在_layout.tsx中给profile页面添加presentation: "fullScreenModal"(iOS端),但会改变profile默认导航行为,仅适用于特定场景:
<Stack.Screen name="profile" options={{ presentation: os === "ios" ? "fullScreenModal" : undefined, // 添加此行 headerShown: true, title: "Profile", headerTitleStyle: { fontFamily: FONTS.bold, fontSize: 24, color: COLORS.black, }, headerTitleAlign: "center", navigationBarHidden: true, headerShadowVisible: false, headerBackVisible: false, }} />
内容的提问来源于stack exchange,提问作者crispengari

