在 React 的上下文 API 中使用 react-router-dom 的意外输出

Unexpected output using react-router-dom with React's Context API

对于我的一个小项目,我正在尝试使用不带 Redux 的 React 上下文 API 尽可能实施最基本的身份验证。

import { createContext, useContext, useState } from 'react'

export const AuthContext = createContext()

export const useAuth = () => {
    const context = useContext(AuthContext)

    if(context === null) throw new Error('Nope')

    return context  
}

export const AuthProvider = (props) => {
    const [authenticated, setAuthenticated] = useState(false)

    const login = () => {
        setAuthenticated(true)

        localStorage.setItem(storageKey, true)
    }
    
    const logout = () => {
        setAuthenticated(false)

        localStorage.setItem(storageKey, false)
    }
    
    return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}

export default AuthContext

我创建了一个上下文,并像这样将我的 <App /> 组件包装在其中; <AuthProvider></App></AuthProvider>。因为我想保持认证状态,所以我使用了浏览器的本地存储,用于存储一个简单的布尔值。

import PrivateRoute from './PrivateRoute'
import { useAuth } from './context/AuthContext'

import { AuthPage } from './pages'

import {
    BrowserRouter,
    Switch,
    Route,
} from 'react-router-dom'

import { useEffect } from 'react'

const App = () => {
    const { login, authenticated } = useAuth()

    useEffect(() => {
        const token = localStorage.getItem('user')

        if(token && token !== false) { login() }
    })

    return (
        <BrowserRouter>
            <Switch>
                <PrivateRoute exact path="/auth" component={AuthPage} />
                <Route exact path='/'>
                    Dashboard
                </Route>
            </Switch>
        </BrowserRouter>
    )
}

export default App

然后,在我的 <App /> 组件中,我尝试调用 AuthProvider 提供的 login 回调,这让我假设它让我在页面刷新期间登录。当我尝试访问当前组件中的 authenticated 变量时,它确实有效。显示我已通过身份验证。

然而,当我尝试设置一个只有经过身份验证的用户才能访问的 PrivateRoute 时:

import {
    Route,
    Redirect
} from 'react-router-dom'

import { useAuth } from './context/AuthContext'

const PrivateRoute = ({ component: Component, ...rest }) => {
    const { authenticated } = useAuth()
    
    if(authenticated) {
        return <Route {...rest} render={(props) => <Component {...props} />} />
    }

    return <Redirect to={{ pathname: '/login' }} />
}

export default PrivateRoute

它不起作用。它只是将我重定向到登录页面。这是怎么来的? PrivateRoute 组件从 <App /> 组件渲染。另外,这个问题的解决方案是什么?

与其在每次重新渲染时 运行 一个 useEffect 来检查用户是否应该登录,不如使用 localStorage 中的值初始化 authenticated 状态:

const storageKey = 'user'
const initialState = JSON.parse(localStorage.getItem(storageKey)) ?? false

export const AuthProvider = (props) => {
  const [authenticated, setAuthenticated] = useState(initialState)

  const login = () => {
      setAuthenticated(true)

      localStorage.setItem(storageKey, true)
  }
  
  const logout = () => {
      setAuthenticated(false)

      localStorage.setItem(storageKey, false)
  }
  
  return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}

感谢 GitHub 上的 Yousaf for the explaination in the comments and the HospitalRun 项目,我在 <App /> 组件中制作了一个加载状态。

import { useAuth } from './context/AuthContext'
import { useEffect, useState } from 'react'

import Router from './Router'

const App = () => {
    const [ loading, setLoading ] = useState(true)
    const { login } = useAuth()

    const token = localStorage.getItem('user')

    useEffect(() => {
        if(token && token !== false) { 
            login()
        }

        setLoading(false)
    }, [loading, token, login])

    if (loading) return null

    return <Router />
}

export default App

这里我只让任何东西渲染,在 login 函数被调用后。

if (loading) return null

如果可以做得更好,我们仍然会收到反馈!