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

TanStack Router中beforeLoad无法获取更新后Auth Context值的问题

问题描述

用户登录后将信息存入AuthContext,进入/addClient或/addProject页面时需校验用户类型。于是在_authenticated.tsx的beforeLoad函数中重新拉取用户信息并更新Context,但遇到以下问题:

  • 在_projectManager.tsx的beforeLoad函数中,context.auth的user为null,调用isProjectManager()无法正确校验权限(尽管用户实际类型是project-manager)
  • _authenticated.tsx的组件内通过useAuth能获取到更新后的用户信息,但路由context中无法获取

项目路由结构

.
📂 routes/
├── 📂 _authenticated/
│   ├── 📂 projectManager/
│   │   ├── addClient.tsx
│   │   └── addProject.tsx
│   └── _projectManager.tsx
├── 📂 _authentication/
│   ├── login.tsx
│   └── signUp.tsx
├── _root.tsx
├── _authenticated.tsx
├── _authentication.tsx
└── index.tsx

相关代码

AuthContext.tsx

import * as React from "react";
import Cookies from "js-cookie";
import { UserType } from "../types/userType";

export type AuthContext = {
  login: (token: string, user: UserType) => void;
  isLogged: () => boolean;
  getAuthToken: () => string | null;
  user: UserType | null;
  setUserDetails: (user: UserType) => void;
  isProjectManager: () => boolean;
};

export const AuthContext = React.createContext<AuthContext | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = React.useState<UserType>({} as UserType);

  function login(token: string, user: UserType) {
    Cookies.set("token", token, { expires: 7 });
    setUser(user);
  }

  function getAuthToken() {
    const token = Cookies.get("token");
    if (!token) {
      console.log("No token found");
      return null;
    }
    return token;
  }

  function isLogged() {
    const token = getAuthToken();
    return !!token;
  }

  function setUserDetails(user: UserType) {
    setUser(user);
  }

  function isProjectManager() {
    console.log(user?.userType);  // 输出undefined,无法获取更新后的user值
    return user?.userType === "project-manager";
  }

  return (
    <AuthContext.Provider
      value={{
        user,
        setUserDetails,
        login,
        isLogged,
        getAuthToken,
        isProjectManager,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

_authenticated.tsx

import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import Navbar from "../components/ui/Navbar";
import { fetchUserDetails } from "../services/apis";
import { useAuth } from "../hooks/useAuth";

export const Route = createFileRoute("/_authenticated")({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isLogged()) {
      throw redirect({ to: "/login" });
    }
    const data = await fetchUserDetails();
    return (context.auth.user = data.user);
  },

  component: AuthenticatedLayout,
});

function AuthenticatedLayout() {
  const auth = useAuth();
  console.log(auth); // 能获取到更新后的用户信息
  return (
    <div>
      <Navbar />
      <Outlet />
    </div>
  );
}

_projectManager.tsx

import { createFileRoute, Outlet } from "@tanstack/react-router";

export const Route = createFileRoute("/_authenticated/_projectManager")({
  beforeLoad: ({ context }) => {
    if (!context.auth.isProjectManager()) {
      console.log(context.auth.isProjectManager()); // 输出false
      throw new Error("You are not authorized to access this page");
    }
  },
  loader: ({ context }) => {
    console.log(context.auth.user); // 输出null或初始空对象
    return context.auth.user;
  },
  component: ProjectManagerLayout,
  errorComponent: () => <div>Unauthorized</div>,
  onError: (error) => {
    console.error(error);
  },
});

function ProjectManagerLayout() {
  return (
    <div>
      <div>Hello /_authenticated/_projectManager!</div>,
      <Outlet />
    </div>
  );
}

原因分析
  1. 路由context与React Context状态不同步:你在_authenticated.tsx的beforeLoad中直接给context.auth.user赋值,这个修改仅作用于路由context对象,并未触发AuthProvider中useState的状态更新(即没调用setUser)。isProjectManager依赖的是React状态的闭包值,因此它读取的始终是初始的空user,而非路由context中更新后的值。
  2. 闭包陷阱:isProjectManager函数在AuthProvider初始化时创建,捕获了当时的user状态。只要AuthProvider不重新渲染(未调用setUser),该函数就会一直使用旧的user值,无法感知路由context的修改。
  3. 初始状态不合理:AuthProvider中user的初始值设为{} as UserType,导致user.userType默认是undefined,进一步干扰权限判断逻辑。

解决方案

方案1:同步路由context与React Context状态

修改_authenticated.tsx的beforeLoad逻辑,调用setUserDetails更新React状态,而非直接修改路由context的user:

// _authenticated.tsx
export const Route = createFileRoute("/_authenticated")({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isLogged()) {
      throw redirect({ to: "/login" });
    }
    const data = await fetchUserDetails();
    // 调用setUserDetails触发React状态更新,同步AuthContext
    context.auth.setUserDetails(data.user);
    // 同步路由context的user值(可选,确保路由层能获取最新值)
    context.auth.user = data.user;
  },
  component: AuthenticatedLayout,
});

方案2:优化AuthProvider避免闭包陷阱

调整AuthProvider的初始状态,并确保isProjectManager每次渲染都捕获最新的user:

// AuthContext.tsx
export function AuthProvider({ children }: { children: React.ReactNode }) {
  // 初始值设为null,逻辑更清晰
  const [user, setUser] = React.useState<UserType | null>(null);

  // ...其他方法不变

  // 每次渲染时重新创建该函数,确保捕获最新的user状态
  function isProjectManager() {
    console.log(user?.userType);
    return user?.userType === "project-manager";
  }

  return (
    <AuthContext.Provider
      value={{
        user,
        setUserDetails,
        login,
        isLogged,
        getAuthToken,
        isProjectManager,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

方案3:在路由层直接校验权限(更可靠)

在_projectManager.tsx的beforeLoad中直接拉取用户信息并校验,避免依赖context的旧值:

// _projectManager.tsx
import { fetchUserDetails } from "../services/apis";

export const Route = createFileRoute("/_authenticated/_projectManager")({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isLogged()) {
      throw redirect({ to: "/login" });
    }
    // 直接拉取最新用户信息进行权限校验
    const data = await fetchUserDetails();
    if (data.user.userType !== "project-manager") {
      throw new Error("You are not authorized to access this page");
    }
    // 同步更新状态
    context.auth.setUserDetails(data.user);
    context.auth.user = data.user;
  },
  // ...其他配置
});

内容的提问来源于stack exchange,提问作者Samiul Karim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 12:45:54