如何让 AuthProvider 中的 useEffect() 先运行再调用 useContext()

How to make useEffect() in AuthProvider runs first then call useContext()

我在我的项目中使用 React,但我在获取用户会话方面遇到了问题。提供者内部的每个组件都可以从这里调用。当浏览器开始加载页面时,user 状态始终为空,直到 axios 在 useEffect() 中完成用户会话。如何控制必须先调用 useEffect() 的 AuthProvider,然后用户状态才能在任何子组件中使用。

这是我的 Auth Context 实现。

import axios from "axios";
import { createContext, useContext, useEffect, useState } from "react"

// Create context to make a context reference.
const authContext = createContext()

// Provider component that warp your app and make auth object
// available to any child component that calls useAuth() 
export function AuthProvider({children}) {

  const [user,setUser] = useState(null)
  const URL_API = "http://localhost:5000/auth"

  useEffect(()=>{
    axios.get(URL_API + "/login")
    .then((response)=>{
      if (response.data.loggedIn === true) {
        setUser(response.data.user)
      }
      else {
        setUser(null)
      }
      console.log(response.data.user)
    })
  },[])

  const login = async (email,password) => {
    const response = await axios.post(URL_API + "/login", { email, password });
    if (response.data.err) {
      throw response.data.err
    }
    if (response.data.loggedIn === true) {
      setUser(response.data.user);
    }
    else {
      setUser(null);
    }
    return response.data.user;
  }

  const register = async (firstname,lastname,email,password) => {
    const response = await axios.post(URL_API + "/register", { firstname, lastname, email, password });
    if (response.data.err) {
      throw response.data.err
    }
    if (response.data.loggedIn === true) {
      setUser(response.data.user);
    }
    else {
      setUser(null);
    }
    return response.data.user;
  }

  const logout = async () => {
    const response = await axios.get(URL_API + "/logout");
    if (response.data.err) {
      throw response.data.err
    }
    if (response.data.loggedIn === true) {
      console.log("Something went wrong!");
    }
    setUser(null);
  }

  return (
    <authContext.Provider value={{user,login,register,logout}}>
      {children}
    </authContext.Provider>
  )
}

// Hook for child components to get the auth object
// and re-render when it changes.
export const useAuth = () => { 
  return useContext(authContext) 
}

如果 auth.user 有一个对象但由于 auth.user 为空而未定义,我在 PublicRoute 中调用了 useAuth() 进行路由。

import { Navigate, Outlet, useLocation } from "react-router-dom"
import { useAuth } from "../Contexts/Auth"

function PublicRoute({firstPath = "/login",redirectPath = "/home",children}) {
  const auth = useAuth()
  const location = useLocation()
  return ( auth.user ? <Navigate to={redirectPath}/> : location.pathname === "/" ? <Navigate to={firstPath}/> : children ? children : <Outlet/> )
}

export default PublicRoute

我正在使用 AuthProvider 覆盖每个 React Router DOM 元素以确保每个路由都可以使用上下文。

这是我的 App.js 用于控制 AuthProvider 与 React Router。

import './App.css';
import axios from 'axios';
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from "react-router-dom"
import Navigator from './Components/Navigator';
import Header from './Components/Header';
import Home from './Page/Home';
import Dashboard from './Page/Dashboard';
import NotFound from './Page/NotFound';
import Login from "./Page/Login"
import Logout from './Page/Logout';
import Register from "./Page/Register"
import ForgotPassword from './Page/ForgotPassword';
import PublicRoute from './Routes/PublicRoute';
import ProtectedRoute from './Routes/ProtectedRoute';
import { SnackbarProvider } from "notistack"
import { AuthProvider } from './Contexts/Auth';

function App() {

  axios.defaults.withCredentials = true

  return (
    <SnackbarProvider maxSnack={3}>
      <AuthProvider>
        <Router>
          <Routes>

            <Route path="/" element={<PublicRoute/>}>
              <Route element={<><Header/><Outlet/></>}>
                <Route path="/register" element={<Register/>}/>
                <Route path='/forgot_password' element={<ForgotPassword/>}/>
              </Route>
            </Route>

            <Route element={<ProtectedRoute roles={["user","admin"]}/>}>
              <Route element={<><Navigator/><Outlet/></>}>
                <Route path="/home" index element={<Home/>}/>
                <Route path="/dashboard" element={<Dashboard/>}/>
                <Route path="/logout" element={<Logout/>}/>
              </Route>
            </Route>

            <Route path="/login" element={<Login/>}/>
            <Route path="*" element={<Navigate to="404"/>}/>
            <Route path="/404" element={<NotFound/>}/>

          </Routes>
        </Router>
      </AuthProvider>
    </SnackbarProvider>
  );
}

export default App;

Here is console log in the browser that PublicRoute is useAuth() before the useEffect() called

我猜你的初始 user 状态和 PublicRoute 组件重定向 before[=26 有问题=] 该应用 实际上 computed/resolved 用户的身份验证状态。

问题是您的 resolved/unauthenticated 状态被您的初始状态值掩盖了。您可能希望使用不确定的初始状态并有条件地呈现 null 或某些加载指示器,直到身份验证状态得到解决。

示例:

export function AuthProvider({ children }) {
  const [user,setUser] = useState(); // <-- initially undefined

  const URL_API = "http://localhost:5000/auth"

  useEffect(() => {
    axios.get(URL_API + "/login")
      .then((response) => {
        if (response.data.loggedIn) {
          setUser(response.data.user);
        } else {
          setUser(null);
        }
        console.log(response.data.user);
      });
  },[]);

  ...

  return (
    <authContext.Provider value={{ user, login, register, logout }}>
      {children}
    </authContext.Provider>
  )
}

公共路线

function PublicRoute({ firstPath = "/login", redirectPath = "/home", children }) {
  const auth = useAuth();
  const location = useLocation();

  if (auth.user === undefined) {
    return null; // or loading indicator, etc...
  }

  return auth.user
    ? <Navigate to={redirectPath} />
    : location.pathname === "/"
      ? <Navigate to={firstPath} />
      : children ? children : <Outlet />;
}