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

Expo Router V2认证后刷新跳转至[...missing]路由问题

问题解决:Expo Router v2 + Firebase Auth 已认证用户刷新跳转到缺失路由

问题描述

使用Expo Router v2结合Firebase Auth实现认证流程,已认证状态下刷新应用会被重定向到[...missing]页面,但创建用户流程无此问题。

核心原因

  1. 布局初始化时机问题:在RootLayoutNav中,当authInitialized为false且user为null时直接返回null,此时Expo Router无法找到匹配的路由,从而触发缺失路由页面。而Firebase的onAuthStateChanged恢复用户状态需要短暂时间,刷新时会进入这个分支。
  2. 路由保护逻辑与状态更新顺序问题:原代码中onAuthStateChanged先更新用户状态再标记认证完成,存在竞态;同时路由保护逻辑虽覆盖了未认证/已认证在Auth组的场景,但布局空白渲染是触发缺失路由的直接原因。

解决方案

1. 修改_layout.jsx,避免初始化前返回null

在认证状态初始化完成前,保持路由结构渲染,同时保留闪屏直到认证状态就绪:

import { SplashScreen, Stack } from "expo-router";
import { Provider, useAuth } from "../context/auth";

export { ErrorBoundary } from "expo-router";

export const unstable_settings = {
  initialRouteName: "(tabs)/home",
};

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  return (
    <Provider>
      <RootLayoutNav />
    </Provider>
  );
}

function RootLayoutNav() {
  const { authInitialized, user } = useAuth();

  // 认证未初始化时,渲染空Stack结构,避免路由缺失
  if (!authInitialized) {
    return <Stack screenOptions={{ headerShown: false }} />;
  }

  // 认证初始化完成后隐藏闪屏
  SplashScreen.hideAsync();

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" options={{ animation: "none" }} />
      <Stack.Screen
        name="(create-account)"
        options={{ presentation: "fullScreenModal" }}
      />
      {/* 注册Auth路由组,确保路由可匹配 */}
      <Stack.Screen name="(auth)" options={{ animation: "none" }} />
    </Stack>
  );
}

2. 完善Auth Context中的状态与路由逻辑

调整状态更新顺序,避免竞态,同时优化路由保护逻辑:

import { useRootNavigation, useRouter, useSegments } from "expo-router";
import { createContext, useContext, useEffect, useState } from "react";
import { FIREBASE_AUTH } from "../config/firebase";
import {
  onAuthStateChanged,
  signInAnonymously,
  signInWithEmailAndPassword,
  signOut,
} from "firebase/auth";

const AuthContext = createContext();

export function Provider({ children }) {
  const [user, setUser] = useState(null);
  const [authInitialized, setAuthInitialized] = useState(false);

  useEffect(() => {
    const subscriber = onAuthStateChanged(FIREBASE_AUTH, (user) => {
      // 先标记认证初始化完成,再更新用户状态,避免竞态
      setAuthInitialized(true);
      setUser(user);
    });
    return () => subscriber();
  }, []);

  const useProtectedRoute = () => {
    const segments = useSegments();
    const router = useRouter();
    const [isNavigationReady, setNavigationReady] = useState(false);
    const rootNavigation = useRootNavigation();

    useEffect(() => {
      const unsubscribe = rootNavigation?.addListener("state", () => {
        setNavigationReady(true);
      });
      return () => unsubscribe?.();
    }, [rootNavigation]);

    useEffect(() => {
      if (!isNavigationReady || !authInitialized) return;

      const inAuthGroup = segments[0] === "(auth)";

      if (!user && !inAuthGroup) {
        // 未认证且不在Auth组,跳转登录页
        router.replace("/(auth)/onboard");
      } else if (user && inAuthGroup) {
        // 已认证且在Auth组,跳转首页
        router.replace("(tabs)/home");
      }
      // 已认证且不在Auth组,保持当前路由无需额外处理
    }, [user, segments, authInitialized, isNavigationReady]);
  };

  const appSignInAnonymously = async () => {
    try {
      await signInAnonymously(FIREBASE_AUTH);
    } catch (error) {
      setUser(null);
      console.log(error);
    }
  };

  const appSignInWithEmailAndPassword = async (email, password) => {
    try {
      await signInWithEmailAndPassword(FIREBASE_AUTH, email, password);
    } catch (error) {
      setUser(null);
      console.log(error);
    }
  };

  const appSignOut = async () => {
    try {
      await signOut(FIREBASE_AUTH);
    } catch (error) {
      console.log(error);
    } finally {
      setUser(null);
    }
  };

  useProtectedRoute();

  return (
    <AuthContext.Provider
      value={{
        appSignInAnonymously,
        appSignInWithEmailAndPassword,
        appSignOut,
        user,
        authInitialized,
      }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const authContext = useContext(AuthContext);
  if (!authContext) {
    throw new Error("useAuth must be used within an AuthContextProvider");
  }
  return authContext;
};

关键修改说明

  • 布局渲染稳定性:认证未完成时不再返回null,而是渲染空Stack结构,确保Router始终有可匹配的路由框架。
  • 状态更新顺序:先标记认证初始化完成,再更新用户状态,避免状态不同步导致的路由判断错误。
  • 路由跳转优化:使用router.replace代替push,避免刷新时路由栈累积,保证路由状态干净。

内容的提问来源于stack exchange,提问作者David Henry

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 16:20:54