React中Refresh Token使用问题:获取新Access Token及页面崩溃修复
React中使用Refresh Token自动刷新Access Token的解决方案
一、处理新Access Token的逻辑
当通过Refresh Token获取到新的Access Token(通常后端会同时返回新的Refresh Token)后,直接调用AuthContext中已有的login方法即可——这个方法已经封装了更新state和localStorage的逻辑,无需重复编写代码。同时要处理刷新失败的情况(比如Refresh Token过期),此时需要执行登出操作。
二、页面刷新时自动修复失效Token
在AuthContext初始化阶段,添加自动刷新Token的逻辑:如果localStorage中存在Refresh Token但Access Token失效,自动调用刷新接口获取新Token。这样页面刷新时就能自动修复无效的Token,避免崩溃。
三、修正组件中的关键问题
1. 修复定时器闭包问题
原setInterval中的reqCounter会陷入闭包陷阱,导致每次递增的值都是初始值,改用函数式更新解决。
2. 修正useEffect依赖
移除不必要的ctx依赖,只保留实际用到的变量,避免不必要的重渲染。
3. 完善错误判断逻辑
原代码仅在catch块设置tokenError,但401/500错误属于HTTP错误,不会触发catch,需要在响应状态判断中主动设置。
修改后的AuthContext代码
import React, { useState, useEffect } from "react"; const AuthContext = React.createContext({ token: "", refreshToken: "", isLoggedIn: false, login: () => {}, logout: () => {}, refreshAuthToken: () => {}, // 新增刷新Token方法 }); export const AuthContextProvider = (props) => { let initialToken = localStorage.getItem("token"); let initialRefreshToken = localStorage.getItem("refresh-token"); const [token, setToken] = useState(initialToken); const [refreshToken, setRefreshToken] = useState(initialRefreshToken); const isUserLoggedIn = !!token; const logoutHandler = () => { setToken(null); setRefreshToken(null); localStorage.removeItem("token"); localStorage.removeItem("books"); localStorage.removeItem("refresh-token"); }; const loginHandler = (token, refreshToken) => { setToken(token); setRefreshToken(refreshToken); localStorage.setItem("token", token); localStorage.setItem("refresh-token", refreshToken); }; // 新增:刷新Token的方法 const refreshAuthToken = async () => { if (!refreshToken) return; try { const response = await fetch("你的刷新Token接口", { method: "POST", body: JSON.stringify({ refresh_token: refreshToken }), headers: { "Content-Type": "application/json" }, }); if (response.ok) { const data = await response.json(); loginHandler(data.access_token, data.refresh_token); // 假设返回access_token和refresh_token } else { logoutHandler(); // 刷新失败,直接登出 } } catch (err) { logoutHandler(); } }; // 页面刷新时,自动检查并刷新Token useEffect(() => { const checkAndRefreshToken = async () => { if (!token && refreshToken) { await refreshAuthToken(); } // 可根据后端逻辑,添加Token有效性验证(比如调用用户信息接口) }; checkAndRefreshToken(); }, []); const contextValue = { token, isLoggedIn: isUserLoggedIn, refreshToken, login: loginHandler, logout: logoutHandler, refreshAuthToken, // 暴露刷新方法给组件 }; return ( <AuthContext.Provider value={contextValue}> {props.children} </AuthContext.Provider> ); }; export default AuthContext;
修改后的Books组件代码
import { useContext, useState, useEffect } from "react"; import AuthContext from "./AuthContext"; import BookItem from "./BookItem"; import PulseLoader from "react-spinners/PulseLoader"; import classes from "./Books.module.css"; const Books = () => { const ctx = useContext(AuthContext); const [books, setBooks] = useState([]); const [reqCounter, setReqCounter] = useState(0); const [tokenError, setTokenError] = useState(false); const [error, setError] = useState(""); // 补全缺失的error状态 const [isLoading, setIsLoading] = useState(false); // 补全缺失的isLoading状态 const fetchData = async () => { setIsLoading(true); setError(""); try { const response = await fetch("你的图书API接口", { method: "GET", headers: { Authorization: `Bearer ${ctx.token}` }, }); if (response.status === 200) { const data = await response.json(); setBooks(data.books); } else if (response.status === 404) { setError("未找到页面"); } else if (response.status === 403) { setError("你无权访问此页面"); } else if (response.status === 401 || response.status === 500) { // 触发Token刷新逻辑 setTokenError(true); } else { setError("请求失败,请稍后重试"); } } catch (err) { setError("网络错误,请检查连接"); setTokenError(true); } finally { setIsLoading(false); } }; // 图书数据请求的useEffect useEffect(() => { if (ctx.isLoggedIn) { fetchData(); } }, [ctx.token, reqCounter, ctx.isLoggedIn]); // 修正依赖,只保留必要变量 // 定时刷新数据(修复闭包问题) useEffect(() => { const interval = setInterval(() => { setReqCounter((prev) => prev + 1); }, 5000); return () => clearInterval(interval); // 添加清理函数,避免内存泄漏 }, []); // Token刷新逻辑 useEffect(() => { const refreshTokenAndRetry = async () => { if (!tokenError) return; await ctx.refreshAuthToken(); // 调用Context中的刷新方法 if (ctx.isLoggedIn) { setReqCounter((prev) => prev + 1); // 刷新成功后,触发图书请求 } setTokenError(false); // 重置错误状态,避免无限循环 }; refreshTokenAndRetry(); }, [tokenError, ctx]); const content = books.map((item) => ( <BookItem title={item.name} year={item.publish_date} pages={item.pages} author={item.Author} img={item.thumbnail} key={item.id || item.name} // 建议用唯一ID,避免名称重复导致的问题 /> )); return ( <section className={classes.bookPage}> {!error && books.length !== 0 && ( <ul className={`list ${classes.booksList}`}>{content}</ul> )} {error && <h2 className={classes.error}>{error}</h2>} {isLoading && ( <PulseLoader color="#f53e3e" className={classes.spinner} /> )} </section> ); }; export default Books;
关键说明
- 新Token处理:通过调用AuthContext的
refreshAuthToken方法,内部会自动调用loginHandler更新state和localStorage,无需手动操作存储。 - 页面刷新修复:AuthContext在初始化时会自动检查Refresh Token并尝试刷新,确保页面刷新后Token始终有效。
- 避免无限循环:每次Token刷新后重置
tokenError,并通过reqCounter触发图书请求,而非直接调用fetchData,保证逻辑清晰。 - 内存泄漏防护:给
setInterval添加清理函数,组件卸载时清除定时器。
内容的提问来源于stack exchange,提问作者ali safaeyan
相关产品推荐
相关产品推荐

