我用Vite+Reaction和SuperBase创建了我的应用程序的框架.

我设置了所有的路由,并创建了一个上下文来了解用户是否登录.

上下文判断本地存储是否有令牌,如果找到,它应该报告给路径‘dashboard’.

一旦登录一切正常,问题是当我刷新时,我被带回登录页面,即使令牌仍然保存在本地存储中.

我被困住了,我解决不了这个问题.有人能帮帮我吗?

我多次try 更改整个路由系统,但我不知道错误在哪里.

我希望在重新加载页面后,如果令牌存在于本地存储中,您会被重定向到‘仪表板’路径,而不是登录

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 {
      //const { from } = location.state || { from: { pathname: "/" } };

      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">
                &ldquo;This library has saved me countless hours of work and
                helped me deliver stunning designs to my clients faster than
                ever before.&rdquo;
              </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 };

推荐答案

Issue

AuthProvider组件使用与未经身份验证的用户相同的初始状态值,因此重定向在页面重新加载时立即生效.换句话说,AuthProvider在初始呈现上提供null标记值,PrivatesRoutes呈现到登录页面的重定向.

Solution

从例如undefined的不确定状态开始,并且有条件地等待,直到在认证上下文中确认/设置认证状态.

interface Tokens {
  access_token: string | undefined| null;
  refresh_token: string | undefined | null;
}

const AuthProvider = ({ children }: AuthProviderProps) => {
  const [tokens, setTokens] = useState<Tokens>({
    access_token: undefined,
    refresh_token: undefined,
  });

  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');

    setTokens({
      access_token: storedAccessToken ?? null,
      refresh_token: storedRefreshToken ?? null
    });
  }, []);

  return (
    <AuthContext.Provider value={{ tokens, setTokens: setToken }}>
      {children}
    </AuthContext.Provider>
  );
};

更新PrivateRoutes以在身份验证状态为确认/设置时有条件地呈现空或某个加载指示符.

const PrivateRoutes = () => {
  const location = useLocation();
  const { tokens } = useContext(AuthContext);

  if (tokens.access_token === undefined) {
    return null; // or loading indicator/spinner/etc
  }

  // tokens.access_token is either null or string at this point
  return tokens.access_token ? (
    <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 }} />
  );
};

另一种方法是使用惰性初始化式函数来设置初始状态值,而不是useEffect挂钩.这将为PrivateRoutes组件的初始渲染周期提供正确的初始状态.

const AuthProvider = ({ children }: AuthProviderProps) => {
  const [tokens, setTokens] = useState<Tokens>(() => {
    const storedAccessToken = localStorage.getItem('access_token');
    const storedRefreshToken = localStorage.getItem('refresh_token');

    return {
      access_token: storedAccessToken ?? null,
      refresh_token: storedRefreshToken ?? null
    };
  });

  const setToken = (accessToken: string | null, refreshToken: string | null) => {
    setTokens({ access_token: accessToken, refresh_token: refreshToken });
  };

  return (
    <AuthContext.Provider value={{ tokens, setTokens: setToken }}>
      {children}
    </AuthContext.Provider>
  );
};

Reactjs相关问答推荐

ReferenceError:未在Redux组件中定义窗口

在Reaction常量函数中未更新状态变量

REACTION 18功能组件正在开发中的S

当列表中的项的顺序改变并且使用唯一键作为它们的索引时,Reaction如何看待呈现方面?

在迭代状态时无法读取未定义的属性(读取 map )

如何在React组件中自动更新图表数据?

使用获取的数据更新状态,但在try console.log 时它给出未定义

强制 useEffect 仅运行一次

如何更新redux状态存在的输入框

如何在使用 React Router v6 将当前页面的状态传递给上一页的同时导航到上一页?

cypress `cy.now()` Type 'Promise' 没有调用签名

使用 react pro 侧边栏展开折叠菜单

在react 中从外部 api 渲染列表

类型错误:类型typeof import(js-cookie)上不存在属性get

添加 React/Redux 组件模板的 VS Code 扩展

list 组件未按预期运行

react-markdown 不渲染 Markdown

如何根据react 中的 map 功能一次只打开一个弹出窗口?

如何在 NavBar 中设置动态标题

a.active 类在导入 x.scss 文件时有效,但在使用 x.module.scss 时无效