React应用调用AutoRefreshToken时setAuth报错(TypeError: setAuth is not a function)的问题排查
问题根源分析
你遇到的 TypeError: setAuth is not a function 报错,核心原因是上下文获取的时机不对:
当你在 AuthProvider 组件里直接调用 const refresh = AutoRefreshToken() 时,这段代码执行在组件的渲染阶段,而此时你的 AuthContext.Provider 还没完成渲染(它在 return 语句里才输出)。也就是说,AutoRefreshToken 里的 useContext(AuthContext) 拿到的是你创建上下文时传入的默认空对象 {},自然 setAuth 就是 undefined,调用它就会抛出错误。
而按钮点击时能正常工作,是因为按钮组件包裹在 AuthContext.Provider 内部,点击触发调用 refresh 时,useContext 已经能正确获取到 Provider 提供的 {auth, setAuth} 对象,所以 setAuth 是有效的函数。
另外还有一个小细节需要注意:useEffect 的回调函数不能直接是 async 的(async 函数会返回 Promise,而 React 期望 useEffect 回调要么返回清理函数,要么不返回任何值),这个问题虽然不是当前报错的原因,但也是需要修正的规范问题。
解决方案
这里提供两种可行的修改方案:
方案一:将 AutoRefreshToken 改为接收参数的工具函数(最直接)
把 setAuth 作为参数传递给刷新函数,彻底避免上下文依赖的时机问题:
// AutoRefreshToken.js 改为工具函数,不再是组件/Hook const autoRefreshToken = (setAuth) => { const refresh = async () => { try { const response = await axios.post("/refresh", { withCredentials: true }); console.log(response.data); setAuth(response.data); } catch (err) { console.log(err); } }; return refresh; }; export default autoRefreshToken;
然后在 AuthProvider 中使用:
import autoRefreshToken from '../hooks/autoRefreshToken'; import { useCallback, useEffect, useState, createContext } from 'react'; export const AuthContext = createContext({}); export const AuthProvider = ({children}) => { const [auth, setAuth] = useState({}); // 用 useCallback 确保 refresh 函数引用稳定 const refresh = useCallback(autoRefreshToken(setAuth), [setAuth]); useEffect(() => { // 把 async 逻辑包裹在内部函数里 const initAuth = async () => { console.log(!auth.user); try { if (!auth.user) { await refresh(); } } catch (err) { console.log(err); } }; initAuth(); }, [auth.user, refresh]) // 依赖 auth.user 和 refresh return( <AuthContext.Provider value={{auth, setAuth, refresh}}> {children} </AuthContext.Provider> ) };
方案二:将 AutoRefreshToken 改为合规的自定义 Hook
遵循 React 自定义 Hook 的命名规范(以 use 开头),并确保在上下文已就绪的环境中使用:
// useAutoRefreshToken.js import { useContext, useCallback } from 'react'; import { AuthContext } from './AuthProvider'; // 注意导入上下文 export const useAutoRefreshToken = () => { const { setAuth } = useContext(AuthContext); const refresh = useCallback(async () => { try { const response = await axios.post("/refresh", { withCredentials: true }); console.log(response.data); setAuth(response.data); } catch (err) { console.log(err); } }, [setAuth]); return refresh; };
然后在 AuthProvider 中使用(注意:必须在 AuthContext.Provider 的范围内调用这个 Hook,所以我们可以把初始化逻辑放到子组件里,或者直接把刷新逻辑整合到 Provider 内部):
import { useAutoRefreshToken } from '../hooks/useAutoRefreshToken'; import { useEffect, useState, createContext } from 'react'; export const AuthContext = createContext({}); export const AuthProvider = ({children}) => { const [auth, setAuth] = useState({}); return( <AuthContext.Provider value={{auth, setAuth}}> {/* 用一个内部组件处理初始化,确保上下文已就绪 */} <AuthInitializer /> {children} </AuthContext.Provider> ) }; // 内部初始化组件 const AuthInitializer = () => { const { auth } = useContext(AuthContext); const refresh = useAutoRefreshToken(); useEffect(() => { const initAuth = async () => { if (!auth.user) { await refresh(); } }; initAuth(); }, [auth.user, refresh]); return null; };
内容的提问来源于stack exchange,提问作者kinhs

