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
相关产品推荐
相关产品推荐

