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

React Native Expo应用无法获取AuthProvider上下文的问题求助

问题描述

我被这个问题折腾好久了,试了所有能想到的办法都解决不了。以下是AuthContext.tsx的代码:

import React, {
  createContext,
  useContext,
  useEffect,
  useState,
  ReactNode,
} from "react";
import axios from "axios";
import * as SecureStore from "expo-secure-store";
import { ActivityIndicator } from "react-native-paper";
import { jwtDecode } from "jwt-decode";

type AuthContextType = {
  onRegister: (username: string, password: string) => Promise<any>;
  onLogin: (username: string, password: string) => Promise<any>;
  onLogout: () => Promise<void>;
  authState: AuthStateType;
  authData: AuthDataType;
};

type AuthStateType = {
  token: string | null;
  authenticated: boolean;
};

type AuthDataType = {
  id: number | null;
  username: string | null;
  iat: number | null;
  exp: number | null;
};

const AuthContext = createContext<AuthContextType | undefined>(undefined);

const TOKEN_KEY = "authToken";
const API_URL = "https://anonytext-backend.vercel.app/api";

const useAuth = (): AuthContextType => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
};

type AuthProviderProps = {
  children: ReactNode;
};

const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const [authState, setAuthState] = useState<AuthStateType>({
    token: null,
    authenticated: false,
  });
  const [authData, setAuthData] = useState<AuthDataType>({
    id: null,
    username: null,
    iat: null,
    exp: null,
  });

  const register = async (username: string, password: string): Promise<any> => {
    try {
      return await axios.post(`${API_URL}/auth/register`, {
        username,
        password,
      });
    } catch (e: any) {
      return {
        error: true,
        message: e.response?.data?.message || "An error occurred",
      };
    }
  };

  const login = async (username: string, password: string): Promise<any> => {
    try {
      const result = await axios.post(`${API_URL}/auth/login`, {
        username,
        password,
      });
      setAuthState({ token: result.data.access, authenticated: true });
      axios.defaults.headers.common[
        "Authorization"
      ] = `Bearer ${result.data.access}`;
      await SecureStore.setItemAsync(TOKEN_KEY, result.data.access);

      return result;
    } catch (e: any) {
      return {
        error: true,
        message: e.response?.data?.message || "An error occurred",
      };
    }
  };

  const logout = async (): Promise<void> => {
    await SecureStore.deleteItemAsync(TOKEN_KEY);
    axios.defaults.headers.common["Authorization"] = "";
    setAuthState({ token: null, authenticated: false });
  };

  useEffect(() => {
    const loadToken = async () => {
      const token = await SecureStore.getItemAsync(TOKEN_KEY);

      if (token) {
        axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
        setAuthState({ token, authenticated: true });
        setAuthData(jwtDecode(token));
      }
      setLoading(false);
    };

    loadToken();
  }, []);

  const contextData: AuthContextType = {
    onRegister: register,
    onLogin: login,
    onLogout: logout,
    authState,
    authData,
  };

  return (
    <AuthContext.Provider value={contextData}>
      {!loading ? children : <ActivityIndicator size={20} />}
    </AuthContext.Provider>
  );
};

export { AuthProvider, useAuth };

我持续收到错误提示:

(NOBRIDGE) ERROR Warning: Error: useAuth must be used within an AuthProvider

按提示,useAuth需要在AuthProvider包裹的子组件中使用,我也是这么做的:

App.tsx代码:

import React from "react";
import { AuthProvider } from "@/context/AuthContext";
import RootLayout from "./app/_layout";
import { NotifierWrapper } from "react-native-notifier";
import { GestureHandlerRootView } from "react-native-gesture-handler";

// Wrap the entire app with AuthProvider
export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <NotifierWrapper>
        <AuthProvider>
          <RootLayout />
        </AuthProvider>
      </NotifierWrapper>
    </GestureHandlerRootView>
  );
}

./app/_layout.tsx代码:

import React, { useCallback } from "react";
import { Slot, Redirect } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { useAuth } from "@/context/AuthContext";
import { ActivityIndicator } from "react-native-paper";

// Prevent the splash screen from hiding until the font is loaded
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const { authState } = useAuth(); // Directly access authState from context

  console.log("Auth Context State:", authState);

  const [fontsLoaded] = useFonts({
    FontAwesome6Pro: require("../assets/fonts/fa-regular-400.ttf"),
    FontAwesome6ProLight: require("../assets/fonts/fa-light-300.ttf"),
    FontAwesome6ProSolid: require("../assets/fonts/fa-solid-900.ttf"),
  });

  const onLayoutRootView = useCallback(async () => {
    if (fontsLoaded) {
      await SplashScreen.hideAsync();
    }
  }, [fontsLoaded]);

  if (!fontsLoaded) {
    return null; // Keep splash screen visible while loading fonts
  }

  // If authState is undefined, don't attempt to render protected routes yet
  if (authState === undefined) {
    return <ActivityIndicator size={20} />; // Show loading indicator if auth state is undefined
  }

  return (
    <GestureHandlerRootView style={{ flex: 1 }} onLayout={onLayoutRootView}>
      {authState.authenticated ? <Slot /> : <Redirect href="/login" />}
    </GestureHandlerRootView>
  );
}

明明已经用AuthProvider包裹了RootLayout,但问题依旧,哪里出错了?


解决方案

核心问题是 Expo Router 的路由机制会优先加载_layout.tsx,此时App.tsx里的AuthProvider还没完成初始化,导致RootLayout调用useAuth时,上下文还未被提供,所以抛出错误。

1. 调整AuthProvider的位置到路由布局内部

修改./app/_layout.tsx,把AuthProvider直接包裹在布局内容外层,确保useAuth调用时上下文已经存在:

import React, { useCallback } from "react";
import { Slot, Redirect } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { AuthProvider, useAuth } from "@/context/AuthContext";
import { ActivityIndicator } from "react-native-paper";

SplashScreen.preventAutoHideAsync();

// 拆分出内部组件处理认证和路由逻辑
const RootLayoutContent = () => {
  const { authState } = useAuth();
  console.log("Auth Context State:", authState);

  const [fontsLoaded] = useFonts({
    FontAwesome6Pro: require("../assets/fonts/fa-regular-400.ttf"),
    FontAwesome6ProLight: require("../assets/fonts/fa-light-300.ttf"),
    FontAwesome6ProSolid: require("../assets/fonts/fa-solid-900.ttf"),
  });

  const onLayoutRootView = useCallback(async () => {
    if (fontsLoaded) {
      await SplashScreen.hideAsync();
    }
  }, [fontsLoaded]);

  if (!fontsLoaded) {
    return null;
  }

  if (!authState) {
    return <ActivityIndicator size={20} />;
  }

  return (
    <GestureHandlerRootView style={{ flex: 1 }} onLayout={onLayoutRootView}>
      {authState.authenticated ? <Slot /> : <Redirect href="/login" />}
    </GestureHandlerRootView>
  );
};

export default function RootLayout() {
  return (
    <AuthProvider>
      <RootLayoutContent />
    </AuthProvider>
  );
}

2. 简化App.tsx

移除App.tsx里的AuthProvider,只保留全局基础包裹:

import React from "react";
import RootLayout from "./app/_layout";
import { NotifierWrapper } from "react-native-notifier";
import { GestureHandlerRootView } from "react-native-gesture-handler";

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <NotifierWrapper>
        <RootLayout />
      </NotifierWrapper>
    </GestureHandlerRootView>
  );
}

额外说明

你的AuthContext.tsx逻辑本身没问题,只是加载时机和Expo Router的路由初始化顺序冲突了。调整AuthProvider的位置后,RootLayoutContent作为AuthProvider的子组件,调用useAuth时就能拿到正常的上下文了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 08:17:32