You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS端expo-router导航失效:从register模态页跳转profile失败

问题:Expo Router iOS端从模态页面跳转至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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 02:24:52