标题可能不明确,但我遇到一个问题,在渲染分配给
/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
时,这怎么可能?我该如何解决这个问题?
视频。
手动更新 URL 时,它会重新加载页面,例如整个 React 应用程序。
Logout
组件在初始渲染时渲染到 "/get-started"
的重定向,并且直到渲染周期结束时才会执行效果。此时,AuthProvider
组件也已安装并运行 its 副作用来检查身份验证状态。
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>;
};
这是事件的基本/粗略概述,因为我能够跟踪它们:
手动将 URL 路径更新为
"/logout"
并重新加载页面
React 应用程序已安装
AuthProvider
安装和渲染
jwt
状态从localStorage
loggedIn
最初是 false路径
"/logout"
已匹配并且 Logout
安装并渲染
Logout
渲染 <Navigate to="/get-started" />
以导航至 "/get-started"
路线"/get-started"
路线渲染 <Navigate to="/get-started/login" />
"/get-started/login"
和 Login
已渲染。我认为大致在这里
Login
和AuthProvider
中的效果都会执行。 AuthProvider
的效果使用初始 jwt
状态进行身份验证检查并更新 loggedIn
状态。 Logout
的效果是清除存储并将状态更新排入队列。
Login
渲染 if (loggedIn) return <Navigate to="/" />;
并导航到 "/"
,因为 loggedIn
在 AuthProvider
auth API 调用解析后为 true
路径
"/"
渲染 App
,并且由于 loggedIn
为 true,因此渲染 <Navigate to="/home" />
,并且 UI 导航并渲染 Home
组件。