React 中上下文状态设置不正确

问题描述 投票:0回答:1

标题可能不明确,但我遇到一个问题,在渲染分配给

/home
的组件后将我重定向到
/logout
,其工作是清除存储并重置
AuthContext
状态。我无法想出一个最小的可重现示例,所以这里是所有代码

这是我的项目结构。

$ tree -I node_modules src
src
├── App.tsx
├── components
│   ├── ContentCenteredDiv.tsx
│   ├── Home.tsx
│   ├── Logout.tsx
│   ├── NavBar.tsx
│   ├── Typewriter.tsx
│   └── ui
│       ├── button.tsx
│       ├── form.tsx
│       ├── input.tsx
│       └── label.tsx
├── contexts
│   └── AuthProvider.tsx
├── hooks
│   └── useAuth.tsx
├── index.css
├── layouts
│   └── get-started
│       ├── GetStartedLayout.tsx
│       ├── Login.tsx
│       └── Register.tsx
├── lib
│   ├── constants.ts
│   ├── schema.ts
│   ├── types.ts
│   └── utils.ts
├── main.tsx
├── vite-env.d.ts
└── wrappers
    └── ProtectedRoute.tsx

9 directories, 23 files

我正在使用react-router来设置导航路线。这是我的

main.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import {
  createBrowserRouter,
  Navigate,
  RouterProvider,
} from "react-router-dom";
import Login from "./layouts/get-started/Login.tsx";
import GetStartedLayout from "./layouts/get-started/GetStartedLayout.tsx";
import Register from "./layouts/get-started/Register.tsx";
import AuthProvider from "./contexts/AuthProvider.tsx";
import ProtectedRoute from "./wrappers/ProtectedRoute.tsx";
import Logout from "./components/Logout.tsx";
import useAuth from "./hooks/useAuth.tsx";
import Home from "@/components/Home.tsx";

const router = createBrowserRouter([
  {
    // path: "/",
    index: true,
    element: <App />,
  },
  {
    path: "get-started",
    element: <GetStartedLayout />,
    children: [
      {
        index: true,
        element: <Navigate to="/get-started/login" />,
      },
      {
        path: "login",
        element: <Login />,
      },
      {
        path: "register",
        element: <Register />,
      },
    ],
  },
  {
    path: "home",
    element: (
      <ProtectedRoute>
        <Home />
      </ProtectedRoute>
    ),
  },
  {
    path: "logout",
    element: <Logout />,
  },
]);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider>
      <RouterProvider router={router} />
    </AuthProvider>
  </StrictMode>,
);

这是包含当前用户身份验证详细信息的

AuthProvider.tsx

import { SERVER_URL } from "@/lib/constants";
import { makePayload } from "@/lib/utils";
import { createContext, PropsWithChildren, useEffect, useState } from "react";

type AuthContextType = {
  checking: boolean;
  loggedIn: boolean;
  setLoggedIn: React.Dispatch<React.SetStateAction<boolean>>;
  userId: number;
  setUserId: React.Dispatch<React.SetStateAction<number>>;
  jwt: string;
  setJwt: React.Dispatch<React.SetStateAction<string>>;
};

const def: AuthContextType = {
  checking: true,
  loggedIn: false,
  setLoggedIn: () => {},
  userId: -1,
  setUserId: () => {},
  jwt: "",
  setJwt: () => {},
};

export const AuthContext = createContext<AuthContextType>(def);

export default function AuthProvider({ children }: PropsWithChildren) {
  const [checking, setChecking] = useState(true);
  const [loggedIn, setLoggedIn] = useState(def.loggedIn);
  const [userId, setUserId] = useState(def.userId);
  const [jwt, setJwt] = useState(localStorage.getItem("jwt") || "");

  useEffect(() => {
    console.log(
      `AuthContext inside useEffect(): checking: ${checking}, loggedIn: ${loggedIn}, userId: ${userId}, jwt:`, // ${jwt}`,
    );

    if (!jwt) {
      setChecking(false);
      return;
    }

    const url = `${SERVER_URL}/api/auth`;
    fetch(url, {
      method: "GET",
      headers: { Authorization: `Bearer ${jwt}` },
    })
      .then((res) => makePayload(res))
      .then((payload) => {
        if (payload.ok) {
          setLoggedIn(true);
          setUserId(payload.userId);
        } else {
          throw `Authorization check failed: ${payload.message || "Unknown error"}`;
        }
      })
      .catch((err) => console.log(`Error fetching ${url}: ${err}`))
      .finally(() => setChecking(false));

  }, []);

  // Store jwt to localstorage when set
  useEffect(() => localStorage.setItem("jwt", jwt), [jwt]);

  return (
    <AuthContext.Provider
      value={{
        checking,
        loggedIn,
        setLoggedIn,
        userId,
        setUserId,
        jwt,
        setJwt,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

这是

useAuth.tsx
,它只是 useContext 的一个简单包装器

import { AuthContext } from "@/contexts/AuthProvider";
import { useContext } from "react";

export default function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error("useAuth must be used within an AuthProvider");
  }

  return ctx;
}

这是

App.tsx
,它根据身份验证状态处理重定向。

import { useEffect } from "react";
import useAuth from "@/hooks/useAuth";
import { Navigate } from "react-router-dom";
import ContentCenteredDiv from "@/components/ContentCenteredDiv";

export default function App() {
  const { loggedIn, checking } = useAuth();

  useEffect(() => {
    return () => {
      sessionStorage.clear();
    };
  }, []);

  if (checking) return <ContentCenteredDiv>Loading...</ContentCenteredDiv>;

  return <Navigate to={loggedIn ? "/home" : "/get-started"} />;
}

下面是映射到路线的组件

/get-started/login

import { createElement, useState } from "react";
import useAuth from "@/hooks/useAuth";
import { SERVER_URL } from "@/lib/constants";
import { makePayload } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";

import { Link, Navigate } from "react-router-dom";
import { loginSchema } from "@/lib/schema";

const formSchema = z.object({
  username: loginSchema.username,
  password: loginSchema.password,
});

export default function Login() {
  const [err, setErr] = useState("");
  const [requesting, setRequesting] = useState(false);
  const [passwordVisible, setPasswordVisible] = useState(false);

  const { setUserId, loggedIn, setLoggedIn, setJwt, jwt } = useAuth();

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      password: "",
    },
  });

  
  // Login handler sets proper AuthContext states after successful login
  function loginHandler({ username, password }: z.infer<typeof formSchema>) {
    setRequesting(true);
    setErr("");
    const url = `${SERVER_URL}/api/login`;
    fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password }),
    })
      .then((res) => makePayload(res))
      .then((payload) => {
        if (payload.ok) {
          setLoggedIn(true);
          setUserId(payload.userId as number);
          setJwt(payload.jwt);
        } else {
          setErr(payload.message);
        }
      })
      .catch((err) => console.log(`Error fetching ${url}: ${err}`))
      .finally(() => setRequesting(false));
  }

  if (loggedIn) return <Navigate to="/" />;

  return (
    // form (is long so I removed it)
  )
}

最后

Logout.tsx
映射到
/logout
Home.tsx
映射到
/home

import { useContext, useEffect, useState } from "react";
import { Navigate } from "react-router-dom";
import useAuth from "../hooks/useAuth";

export default function Logout() {
  const { setLoggedIn, setUserId, checking, loggedIn, userId, setJwt, jwt } =
    useAuth();

  // Reset all states and clear storage
  useEffect(() => {
    console.log(
      `AuthContext during logout: checking: ${checking}, loggedIn: ${loggedIn}, userId: ${userId}, jwt: ${jwt}`,
    );
    localStorage.clear();
    sessionStorage.clear();
    setLoggedIn(false);
    setUserId(-1);
    setJwt("");
    console.log("loggedout");
  }, []);

  return <Navigate to="/get-started" />;
}
import useAuth from "@/hooks/useAuth";

export default function Home() {
  const { setLoggedIn, setUserId, checking, loggedIn, userId, jwt } = useAuth();
  console.log(
    `AuthContext while loading home: checking: ${checking}, loggedIn: ${loggedIn}, userId: ${userId}, jwt: ${jwt}`,
  );
  return "HOME";
}

当我第一次打开

/
时,我会被重定向到
/get-started/login
。登录后,状态已设置,并且我被重定向到
/home
。现在,当我编辑网址栏并将
http://localhost:5173/home
替换为
http://localhost:5173/logout
时,登录页面会闪烁,我再次被重定向到
/home
。 (预计在
/get-started/login

在此

/logout
->
/get-started/login
->
/home
期间,我的后端也收到了身份验证请求,这意味着
useEffect()
中的
AuthProvider
再次运行。获取请求向服务器发送有效的 jwt 令牌。当我
setJwt("")
Logout
时,这怎么可能?我该如何解决这个问题?

视频

reactjs react-hooks local-storage react-context
1个回答
0
投票

问题

手动更新 URL 时,它会重新加载页面,例如整个 React 应用程序。

Logout
组件在初始渲染时渲染到
"/get-started"
的重定向,并且直到渲染周期结束时才会执行效果。此时,
AuthProvider 
组件也已安装并运行 its 副作用来检查身份验证状态。

解决方案

TL;DR 原因

TL;DR 是从

"/logout"
"/get-started"
的导航发生在
Logout
组件的
useEffect
钩子回调执行之前,因此所有身份验证逻辑都会在
 之前查看当前存储的 JWT 令牌数据Logout
效果已清除。

修复建议

setLoggedIn

setUserId
setJwt
的排队状态更新有机会被 React 处理并触发重新渲染后,您应该延迟注销导航的发生
至少
一个渲染周期流程可以运行并读取更新的状态以及本地和会话存储值。

简单的解决方案是使用延迟尽可能短的

setTimeout
,这足以将超时回调放置在 Javascript 事件循环的末尾并允许 React 更新。

示例:

注销.tsx

import { useNavigate } from 'react-router-dom';

const Logout = () => {
  const navigate = useNavigate();

  const { setLoggedIn, setUserId, setJwt } = useAuth();

  // Reset all states and clear storage
  useEffect(() => {
    localStorage.clear();
    sessionStorage.clear();

    // Enqueue React state updates
    setLoggedIn(false);
    setUserId(-1);
    setJwt("");

    // Place callback to end of event loop to navigate after
    // React updates are processed
    setTimeout(() => navigate("/get-started"), 0);
  }, []);

  // Return & render anything really, anything but Navigate
  return <div>Logging out...</div>;
};

非 TL;DR 解释

这是事件的基本/粗略概述,因为我能够跟踪它们:

  1. 手动将 URL 路径更新为

    "/logout"
    并重新加载页面

  2. React 应用程序已安装

  3. AuthProvider
    安装和渲染

    • jwt
      状态从
      localStorage
    • 初始化
    • loggedIn
      最初是 false
  4. 路径

    "/logout"
    已匹配并且
    Logout
    安装并渲染

    • Logout
      渲染
      <Navigate to="/get-started" />
      以导航至
      "/get-started"
      路线
    • "/get-started"
      路线渲染
      <Navigate to="/get-started/login" />
    • "/get-started/login"
      Login
      已渲染。
  5. 认为大致在这里

    Login
    AuthProvider
    中的效果都会执行。
    AuthProvider
    的效果使用初始
    jwt
    状态进行身份验证检查并更新
    loggedIn
    状态。
    Logout
    的效果是清除存储并将状态更新排入队列。

  6. Login
    渲染
    if (loggedIn) return <Navigate to="/" />;
    并导航到
    "/"
    ,因为
    loggedIn
    AuthProvider
    auth API 调用解析后为 true

  7. 路径

    "/"
    渲染
    App
    ,并且由于
    loggedIn
    为 true,因此渲染
    <Navigate to="/home" />
    ,并且 UI 导航并渲染
    Home
    组件。

© www.soinside.com 2019 - 2024. All rights reserved.