React Hook中tokenFn无法捕获token更新返回空值问题求助
解决useToken2 Hook无法获取有效token的问题
问题场景
你编写了用于获取认证token的useToken2 Hook,以及调用它的Test组件,但返回的token始终为空。日志显示useEffect setToken执行,但最终触发tokenFn timeout返回空值。核心原因是status === 'authenticated'在应用启动后一段时间才会被设置,而你用setInterval监听token变化的逻辑因闭包问题无法生效。
原Hook代码
export function useToken2() { const { data: session, status } = useSession(); const [token, setToken] = useState<string | null>(null); useEffect(() => { if (status === 'authenticated' && session?.accessToken) { logger('useEffect setToken') setToken(session.accessToken); } }, [status, session]); const tokenFn = useCallback(async (): Promise<string> => { return new Promise<string>((resolve) => { if (token != null) { resolve(token); } else { const tokenInterval = setInterval(() => { if (token != null) { clearInterval(tokenInterval); resolve(token); } }, 100); setTimeout(() => { clearInterval(tokenInterval); logger('tokenFn timeout') resolve(''); }, 5000); } }); }, [token]); return { tokenFn, }; }
原Test组件代码
const Test: FC = () => { const [token, setToken] = useState(''); const {tokenFn} = useToken2() useEffect(() => { tokenFn().then((res) => { setToken(res) }) }, []); return ( <> {token} </> ); };
问题根因
tokenFn是通过useCallback创建的,它的依赖是token。当token还为null时,tokenFn就被创建,此时它的闭包捕获的是null值的token。后续status变为authenticated后,token被更新,但旧的tokenFn里的token依然是闭包中的旧值,因此setInterval里的判断永远不会成立,最终触发超时返回空。
解决方案
方案一:用useRef保存resolve函数,主动触发
修改useToken2 Hook,使用useRef保存等待中的resolve函数,当token可用时直接调用该函数返回结果:
export function useToken2() { const { data: session, status } = useSession(); const [token, setToken] = useState<string | null>(null); // 保存等待中的resolve函数 const resolveRef = useRef<((value: string) => void) | null>(null); useEffect(() => { if (status === 'authenticated' && session?.accessToken) { logger('useEffect setToken'); setToken(session.accessToken); // 如果有等待的请求,直接返回token if (resolveRef.current) { resolveRef.current(session.accessToken); resolveRef.current = null; } } }, [status, session]); const tokenFn = useCallback(async (): Promise<string> => { return new Promise<string>((resolve) => { if (token != null) { resolve(token); } else { // 记录当前resolve,等待token就绪 resolveRef.current = resolve; // 超时兜底逻辑 setTimeout(() => { // 确保只处理当前的resolve请求 if (resolveRef.current === resolve) { resolveRef.current = null; logger('tokenFn timeout'); resolve(''); } }, 5000); } }); }, [token]); return { tokenFn, }; }
方案二:优化Test组件的依赖
同时可以优化Test组件的useEffect依赖,当tokenFn更新时重新调用它,确保获取最新的token:
const Test: FC = () => { const [token, setToken] = useState(''); const { tokenFn } = useToken2(); useEffect(() => { let isMounted = true; tokenFn().then((res) => { // 防止组件卸载后更新状态 if (isMounted) { setToken(res); } }); return () => { isMounted = false; }; }, [tokenFn]); // 依赖tokenFn,更新时重新执行 return ( <> {token} </> ); };
方案三:去掉冗余state,直接基于session判断
如果不需要额外存token的state,还可以直接基于session和status处理,减少状态冗余:
export function useToken2() { const { data: session, status } = useSession(); const resolveRef = useRef<((value: string) => void) | null>(null); useEffect(() => { if (status === 'authenticated' && session?.accessToken) { logger('useEffect setToken'); if (resolveRef.current) { resolveRef.current(session.accessToken); resolveRef.current = null; } } }, [status, session]); const tokenFn = useCallback(async (): Promise<string> => { return new Promise<string>((resolve) => { if (status === 'authenticated' && session?.accessToken) { resolve(session.accessToken); } else { resolveRef.current = resolve; setTimeout(() => { if (resolveRef.current === resolve) { resolveRef.current = null; logger('tokenFn timeout'); resolve(''); } }, 5000); } }); }, [status, session]); return { tokenFn, }; }
说明
以上方案都解决了闭包导致的token值无法更新的问题,当token就绪时会主动通知等待的Promise,确保tokenFn能获取到有效token。
内容的提问来源于stack exchange,提问作者Evgeniy Sokolov
相关产品推荐
相关产品推荐

