React Router Dom v6无法维持Supabase Context:刷新跳转登录页问题
问题描述
我基于Vite+React+Supabase搭建了应用骨架,配置了路由并创建Auth Context用于判断用户登录状态。该Context会检查localStorage中的tokens,若存在则应跳转至'dashboard'路径。登录后功能运行正常,但页面刷新时,即使localStorage中仍保存着tokens,仍会被重定向到登录页。多次调整路由系统仍未定位到问题,希望实现:页面刷新时若localStorage中存在token,自动跳转至'dashboard'而非登录页。
App.tsx
import { createBrowserRouter, Navigate, Outlet, RouterProvider, useLocation, } from "react-router-dom"; import { AuthContext } from "./context/AuthContext"; import { AuthProvider } from "./context/AuthContext"; import Login from "./pages/login/login"; import { useContext } from "react"; import Dashboard from "./pages/Dashboard/dashboard"; import { Sidebar } from "./components/custom/sidebar"; const PrivateRoutes = () => { const location = useLocation(); const { tokens } = useContext(AuthContext); return tokens.access_token !== null ? ( <div className="bg-background h-screen"> <div className="grid lg:grid-cols-5 h-screen"> <Sidebar className="hidden lg:flex lg:flex-col h-screen" /> <div className="col-span-3 lg:col-span-4 lg:border-l"> <div className="h-full px-4 py-6 lg:px-8"> <Outlet /> </div> </div> </div> </div> ) : ( <Navigate to="/login" replace state={{ from: location }} /> ); }; const router = createBrowserRouter([ { path: "/", element: <PrivateRoutes />, children: [ { path: "dashboard", element: <Dashboard />, }, ], }, { path: "/login", element: <Login />, }, ]); function App() { return ( <> <AuthProvider> <RouterProvider router={router} /> </AuthProvider> </> ); } export default App;
Login.tsx
import { useContext } from "react"; import { AuthContext } from "@/context/AuthContext"; import { cn } from "@/lib/utils"; import { useSignal } from "@preact/signals-react"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ReloadIcon } from "@radix-ui/react-icons"; import { Toaster, toast } from "sonner"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { signIn, supabase } from "@/utils/supabase"; import { useNavigate } from "react-router-dom"; const formSchema = z.object({ email: z.string().email({ message: "Email address is not valid", }), password: z.string().min(8, { message: "Password must be at least 8 characters.", }), }); export default function Login() { const { setTokens } = useContext(AuthContext); const isLoading = useSignal(false); const navigate = useNavigate(); const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), }); const onSubmit = async (data: z.infer<typeof formSchema>) => { isLoading.value = true; const res = await signIn(data); if (res.error) toast.error("Errore, credenziali errate"); else { setTokens(res.data.session.access_token, res.data.session.refresh_token); localStorage.setItem("access_token", res.data.session.access_token); localStorage.setItem("refresh_token", res.data.session.access_token); supabase.auth.setSession({ access_token: res.data.session.access_token, refresh_token: res.data.session.refresh_token }); navigate('/dashboard'); } isLoading.value = false; }; return ( <> <div className="container h-screen relative hidden flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0"> <Button asChild className={cn("absolute right-4 top-4 md:right-8 md:top-8")}> <a href="auth">Register</a> </Button> <div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex"> <div className="absolute inset-0 bg-zinc-900" /> <div className="relative z-20 flex items-center text-lg font-medium"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2 h-6 w-6" > <path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" /> </svg> Acme Inc </div> <div className="relative z-20 mt-auto"> <blockquote className="space-y-2"> <p className="text-lg"> “This library has saved me countless hours of work and helped me deliver stunning designs to my clients faster than ever before.” </p> <footer className="text-sm">Sofia Davis</footer> </blockquote> </div> </div> <div className="lg:p-8"> <div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[450px]"> <div className="flex flex-col space-y-2"> <h1 className="text-2xl font-semibold tracking-tight mb-2 text-center"> Login in your account </h1> </div> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-y-3" > <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input placeholder="Enter your email" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <Input placeholder="Enter your password" type="password" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <div className="pt-6 flex w-full"> <Button className="w-full" type="submit" disabled={isLoading.value} > {isLoading.value === false ? ( <> <span>Login</span> </> ) : ( <> <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> please wait </> )} </Button> </div> </form> </Form> </div> </div> </div> <Toaster /> </> ); }
AuthContext.tsx
import { createContext, useState, useEffect, ReactNode } from 'react'; interface Tokens { access_token: string | null; refresh_token: string | null; } interface AuthContextProps { tokens: Tokens; setTokens: (accessToken: string | null, refreshToken: string | null) => void; } const initialAuthContext: AuthContextProps = { tokens: { access_token: null, refresh_token: null, }, setTokens: () => {}, }; const AuthContext = createContext<AuthContextProps>(initialAuthContext); interface AuthProviderProps { children: ReactNode; } const AuthProvider = ({ children }: AuthProviderProps) => { const [tokens, setTokens] = useState<Tokens>({ access_token: null, refresh_token: null, }); const setToken = (accessToken: string | null, refreshToken: string | null) => { setTokens({ access_token: accessToken, refresh_token: refreshToken }); }; // Check for tokens in local storage on component mount useEffect(() => { const storedAccessToken = localStorage.getItem('access_token'); const storedRefreshToken = localStorage.getItem('refresh_token'); if (storedAccessToken && storedRefreshToken) { setTokens({ access_token: storedAccessToken, refresh_token: storedRefreshToken }); } console.log(tokens); }, []); return ( <AuthContext.Provider value={{ tokens, setTokens: setToken }}> {children} </AuthContext.Provider> ); }; export { AuthContext, AuthProvider };
解决方案
问题根源
- 异步状态更新导致路由判断过早:页面刷新时,
RouterProvider先于AuthProvider的useEffect执行完成,此时tokens还是初始的null值,PrivateRoutes直接触发跳转到登录页,等useEffect读取localStorage更新状态时,跳转已经完成。 - Login组件的refresh_token存储错误:代码中把access_token重复存到了refresh_token的key下,导致localStorage里的refresh_token值错误,
AuthContext读取时会因为refresh_token无效不更新状态。
修复步骤
1. 修正Login.tsx中的token存储错误
找到onSubmit函数里的这行代码:
localStorage.setItem("refresh_token", res.data.session.access_token);
改为:
localStorage.setItem("refresh_token", res.data.session.refresh_token);
2. 在AuthContext中添加加载状态,避免初始错误跳转
修改AuthContext.tsx,添加isLoading状态,在读取localStorage完成前不进行路由判断:
import { createContext, useState, useEffect, ReactNode } from 'react'; import { supabase } from '@/utils/supabase'; interface Tokens { access_token: string | null; refresh_token: string | null; } interface AuthContextProps { tokens: Tokens; setTokens: (accessToken: string | null, refreshToken: string | null) => void; isLoading: boolean; } const initialAuthContext: AuthContextProps = { tokens: { access_token: null, refresh_token: null, }, setTokens: () => {}, isLoading: true, }; const AuthContext = createContext<AuthContextProps>(initialAuthContext); interface AuthProviderProps { children: ReactNode; } const AuthProvider = ({ children }: AuthProviderProps) => { const [tokens, setTokens] = useState<Tokens>({ access_token: null, refresh_token: null, }); const [isLoading, setIsLoading] = useState(true); const setToken = (accessToken: string | null, refreshToken: string | null) => { setTokens({ access_token: accessToken, refresh_token: refreshToken }); }; useEffect(() => { const storedAccessToken = localStorage.getItem('access_token'); const storedRefreshToken = localStorage.getItem('refresh_token'); if (storedAccessToken && storedRefreshToken) { setTokens({ access_token: storedAccessToken, refresh_token: storedRefreshToken }); // 同步Supabase会话状态 supabase.auth.setSession({ access_token: storedAccessToken, refresh_token: storedRefreshToken }); } setIsLoading(false); }, []); return ( <AuthContext.Provider value={{ tokens, setTokens: setToken, isLoading }}> {children} </AuthContext.Provider> ); }; export { AuthContext, AuthProvider };
3. 修改PrivateRoutes组件,处理加载状态
在PrivateRoutes中,判断isLoading状态,加载时显示加载提示,避免直接跳转:
const PrivateRoutes = () => { const location = useLocation(); const { tokens, isLoading } = useContext(AuthContext); if (isLoading) { return <div>加载中...</div>; // 可替换为项目自定义加载组件 } return tokens.access_token !== null ? ( <div className="bg-background h-screen"> <div className="grid lg:grid-cols-5 h-screen"> <Sidebar className="hidden lg:flex lg:flex-col h-screen" /> <div className="col-span-3 lg:col-span-4 lg:border-l"> <div className="h-full px-4 py-6 lg:px-8"> <Outlet /> </div> </div> </div> </div> ) : ( <Navigate to="/login" replace state={{ from: location }} /> ); };
4. 优化路由配置,根路径默认跳转至dashboard
修改路由配置,给根路径添加index跳转,避免用户访问根路径时无内容:
const router = createBrowserRouter([ { path: "/", element: <PrivateRoutes />, children: [ { index: true, element: <Navigate to="/dashboard" replace /> }, { path: "dashboard", element: <Dashboard />, }, ], }, { path: "/login", element: <Login />, }, ]);
额外说明
- 建议使用Supabase官方的
authStateChange监听状态变化,而非手动管理localStorage,能更好同步Supabase的auth状态,避免手动管理token带来的不一致问题。 - 加载状态的UI可替换为项目中的自定义加载组件,提升用户体验。
内容的提问来源于stack exchange,提问作者Filippo Brigati
相关产品推荐
相关产品推荐

