React自定义useAuth Hook异步获取isAdmin状态异常问题
问题分析
你的核心问题出在两个地方:
useAuth中token是初始化时直接从localStorage读取的静态值,登录后localStorage更新但该变量不会自动重新获取,导致解析权限的useEffect不触发。- React Router 的
createBrowserRouter是在App组件初始化时静态创建的,后续isAdmin状态变化不会重新生成路由配置,所以/admin路由的渲染逻辑不会更新。
解决方案
1. 重构 useAuth Hook,让状态完全响应式
将 token 改为 useState 状态,监听 localStorage 变化并主动处理 token 解析、过期校验,同时添加加载状态避免初始错误渲染:
import { useState, useEffect } from "react"; import { jwtDecode } from "jwt-decode"; const useAuth = () => { // 初始化时从localStorage读取token,作为响应式状态 const [token, setToken] = useState(localStorage.getItem("token")); const [isLoggedIn, setIsLoggedIn] = useState(!!token); const [isAdmin, setIsAdmin] = useState(false); const [isLoading, setIsLoading] = useState(true); // 解析token并更新权限与登录状态 useEffect(() => { const decodeToken = () => { setIsLoading(true); if (!token) { setIsAdmin(false); setIsLoggedIn(false); setIsLoading(false); return; } try { const decodedToken = jwtDecode(token); // 校验token是否过期 if (decodedToken.exp * 1000 > Date.now()) { setIsAdmin(decodedToken.isAdmin); setIsLoggedIn(true); } else { // token过期自动登出 logoutHandler(); } } catch (err) { // token解析失败视为无效 logoutHandler(); } setIsLoading(false); }; decodeToken(); }, [token]); // 监听localStorage变化(支持多标签页同步状态) useEffect(() => { const handleStorageChange = (e) => { if (e.key === "token") { setToken(e.newValue); } }; window.addEventListener("storage", handleStorageChange); return () => window.removeEventListener("storage", handleStorageChange); }, []); const logoutHandler = () => { localStorage.removeItem("token"); localStorage.removeItem("userId"); setToken(null); setIsAdmin(false); setIsLoggedIn(false); }; // 登录成功后主动更新token状态的方法 const loginHandler = (newToken) => { localStorage.setItem("token", newToken); setToken(newToken); }; return { logoutHandler, loginHandler, isLoggedIn, isAdmin, isLoading }; }; export default useAuth;
2. 修改路由权限校验方式,避免静态路由限制
React Router 的路由配置是静态生成的,不能直接依赖组件状态,推荐用路由保护组件或Loader 权限校验处理:
方案A:路由保护组件(适合客户端渲染场景)
创建一个 AdminGuard 组件,在组件内部实时校验权限:
// AdminGuard.jsx import { Navigate } from "react-router-dom"; import useAuth from "./useAuth"; const AdminGuard = ({ children }) => { const { isAdmin, isLoading } = useAuth(); if (isLoading) { return <div>加载中...</div>; // 展示加载状态避免错误提示 } return isAdmin ? children : <Navigate to="/" replace />; }; export default AdminGuard;
然后修改 App.jsx 的路由配置:
import { createBrowserRouter, RouterProvider, Fragment } from "react-router-dom"; import RootLayout from "./RootLayout"; import HomePage from "./HomePage"; import SignupPage from "./SignupPage"; import LoginPage from "./LoginPage"; import AdminPage from "./AdminPage"; import AdminGuard from "./AdminGuard"; import { fetchMealsLoader, adminMealsLoader, signUpAction, loginAction, sendMealDataAction } from "./actions"; function App() { const router = createBrowserRouter([ { path: "/", element: <RootLayout />, children: [ { index: true, element: <HomePage />, loader: fetchMealsLoader }, { path: "auth/signup", element: <SignupPage />, action: signUpAction }, { path: "auth/login", element: <LoginPage />, action: loginAction }, { path: "admin", element: ( <AdminGuard> <AdminPage /> </AdminGuard> ), loader: adminMealsLoader, action: sendMealDataAction, }, ], }, ]); return ( <Fragment> <RouterProvider router={router} /> </Fragment> ); } export default App;
方案B:Loader 权限校验(更推荐,支持提前拦截)
在 /admin 路由的 Loader 中直接校验权限,不满足则抛出错误并通过 errorElement 处理:
// 修改adminMealsLoader import { jwtDecode } from "jwt-decode"; export async function adminMealsLoader() { const token = localStorage.getItem("token"); if (!token) { throw new Response(null, { status: 401, statusText: "Unauthorized" }); } try { const decodedToken = jwtDecode(token); if (decodedToken.exp * 1000 <= Date.now() || !decodedToken.isAdmin) { throw new Response(null, { status: 403, statusText: "Forbidden" }); } } catch (err) { throw new Response(null, { status: 403, statusText: "Forbidden" }); } // 原有数据加载逻辑 const response = await fetch("你的管理员API地址"); if (!response.ok) { throw new Response(null, { status: 500, statusText: "加载失败" }); } return response.json(); }
更新路由配置:
{ path: "admin", element: <AdminPage />, loader: adminMealsLoader, action: sendMealDataAction, errorElement: <NotAdminError />, }
3. 登录成功后主动同步状态
在登录 Action 中调用 useAuth 的 loginHandler 方法,确保状态实时更新:
// 示例loginAction export async function loginAction({ request }) { const formData = await request.formData(); const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: formData.get("email"), password: formData.get("password"), }), }); const data = await response.json(); if (!response.ok) return { error: data.message }; // 调用useAuth的loginHandler更新状态 const { loginHandler } = useAuth(); loginHandler(data.token); localStorage.setItem("userId", data.userId); return redirect("/"); }
内容的提问来源于stack exchange,提问作者Priyansh
相关产品推荐
相关产品推荐

