You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React自定义useAuth Hook异步获取isAdmin状态异常问题

问题分析

你的核心问题出在两个地方:

  1. useAuth 中 token 是初始化时直接从 localStorage 读取的静态值,登录后 localStorage 更新但该变量不会自动重新获取,导致解析权限的 useEffect 不触发。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 00:38:10