如何每隔t分钟自动刷新JWT token并修复useEffect中定时器异常问题
问题原因与修复方案
现有代码的核心问题
- 闭包陷阱:useEffect依赖数组为空,只会在组件第一次挂载时执行,内部捕获的
refreshToken、history都是初始值,如果挂载时refreshToken还未生成(比如登录状态还没初始化完成),后续refreshToken更新后useEffect也不会重新执行,定时器内部一直拿不到有效refreshToken,请求直接失败,看起来就像定时器没启动。 - 定时器ID丢失:你没有在useEffect的清理函数中清除定时器,且
refresh变量只存在于第一次useEffect的作用域内,后续如果组件重渲染、路由跳转后根组件实例变化,旧的定时器ID就会丢失,自然无法清除,导致定时器一直运行。 - 异步请求堆叠:setInterval不管上一次请求是否完成,到点就发新请求,如果接口响应慢超过设置的间隔时间,会出现多个请求同时发送的异常情况,也会导致逻辑混乱。
修复后的实现代码
推荐用setTimeout递归调用替代setInterval,避免请求堆叠,同时用ref存储定时器ID保证能正常清除,补全依赖数组和清理逻辑:
import { useRef, useEffect } from 'react' // 用ref存储定时器ID,跨渲染周期保持引用 const refreshTimerRef = useRef(null) useEffect(() => { // 无有效refreshToken时不启动刷新逻辑 if (!refreshToken) return async function getRefreshToken() { try { const response = await axios.post( "http://localhost:3001/refreshToken", { refreshToken }, { withCredentials: true } ) if (response.data.redirect) { // 先清除定时器再处理跳转 if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current) refreshTimerRef.current = null } localStorage.clear() history.replace(response.data.redirect) return } // 请求成功后设置下一次刷新,此处替换为你需要的间隔时间 refreshTimerRef.current = setTimeout(getRefreshToken, 4 * 60 * 1000) } catch (err) { // 接口报错也需要清除定时器,跳转登录 if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current) refreshTimerRef.current = null } localStorage.clear() history.replace('/login') } } // 启动第一次刷新 refreshTimerRef.current = setTimeout(getRefreshToken, 4 * 60 * 1000) // 组件卸载或依赖变化时自动清除定时器 return () => { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current) refreshTimerRef.current = null } } }, [refreshToken, history]) // 把用到的外部变量加入依赖数组
更优的JWT刷新方案
定时轮询的性能消耗较高,推荐用被动刷新方案:
- 接口响应触发:在axios响应拦截器中全局捕获401状态码,自动触发refreshToken逻辑,刷新成功后重发之前失败的请求,刷新失败再清空本地状态跳转到登录页。
- 提前预判刷新:在请求拦截器中每次发送请求前,判断accessToken的过期时间,如果距离过期不足1分钟(可自定义阈值),就先执行刷新逻辑,拿到新的token后再发送当前请求,避免出现无效的401请求。
- 全局状态托管:把token状态和刷新逻辑托管到全局状态管理工具(如Zustand、Redux)中,和组件生命周期解绑,避免组件挂载/卸载导致的定时器异常问题。
内容的提问来源于stack exchange,提问作者Arcanus
相关产品推荐
相关产品推荐

