You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React组件间持久化useContext?登录跳转异常求助

问题描述

登录验证通过后,导航至其他组件时会被重定向回登录页面,推测是useContext使用不当导致状态断开。参考过相关教程但中途卡壳,怀疑缺少useEffect的正确实现,需要帮助排查问题。

代码文件

App.js

import './App.css';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import LoggedIn from './NavBar/LoggedIn';
import Home from './Component/Home';
import { About } from './Component/About';
import { Blog } from './Component/Blog';
import { Products } from './Component/Products';
import { Pricing } from './Component/Pricing';
import { LoginPage } from './Component/LoginPage';
import { AuthProvider } from './utils/UserContext'
import PrivateRoute from './Component/PrivateRoute'

function App() {
  return (
    <>
      <BrowserRouter>
        <AuthProvider>
          <LoggedIn />
          <Routes>
            <Route path='/' element={<LoginPage />} />
            <Route path='/home' element={<PrivateRoute><Home /></PrivateRoute>} />
            <Route path='/about' element={<PrivateRoute><About /></PrivateRoute>} />
            <Route path='/blog' element={<PrivateRoute><Blog /></PrivateRoute>} />
            <Route path='/products' element={<PrivateRoute><Products /></PrivateRoute>} />
            <Route path='/pricing' element={<PrivateRoute><Pricing /></PrivateRoute>} />
          </Routes>
        </AuthProvider>
      </BrowserRouter>
    </>
  );
}

export default App;

UserContext.js

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

const UserContext = createContext();

export function useAuth(){
    return useContext(UserContext)
}

export function AuthProvider(props){
    const [authUser, setAuthUser] = useState(null);
    const [isLoggedIn, setIsLoggedIn] = useState(false);

    // 初始化时从localStorage读取登录状态
    useEffect(() => {
        const savedUser = localStorage.getItem('authUser');
        const savedLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
        if(savedUser && savedLoggedIn){
            setAuthUser(savedUser);
            setIsLoggedIn(true);
        }
    }, []);

    // 状态变化时保存到localStorage
    useEffect(() => {
        if(isLoggedIn && authUser){
            localStorage.setItem('authUser', authUser);
            localStorage.setItem('isLoggedIn', 'true');
        } else {
            localStorage.removeItem('authUser');
            localStorage.removeItem('isLoggedIn');
        }
    }, [authUser, isLoggedIn]);

    const value = {
        authUser,
        setAuthUser,
        isLoggedIn,
        setIsLoggedIn
    }

    return <UserContext.Provider value={value}>{props.children}</UserContext.Provider>
}

LoginPage.js

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button'
import '../CSS/LoginPageCSS.css'
import { useAuth  } from '../utils/UserContext';

export function LoginPage() {
    const navigate = useNavigate();
    const { setIsLoggedIn, setAuthUser } = useAuth()
    const [loginAttempts, setLoginAttempts] = useState(3); 

    const handleLogin = (event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        validateLogin(data.get("username"), data.get("password"));
    }

    const validateLogin = async (username, password) => {
        const res = await fetch(`http://localhost:8000/api/users/${username}`);
        const data = await res.json();
        const pswrd = data.user.password;

        if (password !== pswrd) {
            setLoginAttempts(prev => prev - 1);
            console.log("ATTMEPTS:", loginAttempts, "Password", password, "pswrd:", pswrd);
        } else {
            setIsLoggedIn(true);
            setAuthUser(username);
            navigate('/home');
        }
    }

    return (
        <div>
            {
                loginAttempts > 0 ? (
                    <div className='LoginPage_container'>
                        <div>
                            <h2>Welcome to THE SPACE</h2>
                            <p>Login to get started</p>
                        </div>
                        <Box
                            onSubmit={handleLogin}
                            component="form"
                            sx={{
                                '& .MuiTextField-root': { m: 1, width: '25ch' },
                            }}
                            noValidate
                            autoComplete="off"
                        >
                            <div>
                                <TextField
                                    id="username"
                                    name="username"
                                    label="Username"
                                />
                                <TextField
                                    id="password"
                                    name="password"
                                    label="Password"
                                />
                            </div>
                            <Button
                                type="submit"
                                variant="contained"
                            >
                                Sign In
                            </Button>
                        </Box>
                    </div>
                ) : <h1>Too many attempts, aborting</h1>
          }
        </div>
    )
}

LoggedIn.js(登录验证通过后跳转的受保护导航栏,仅登录用户可见)

import React, { useEffect, useState } from "react";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu";
import MenuIcon from "@mui/icons-material/Menu";
import Container from "@mui/material/Container";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import AdbIcon from "@mui/icons-material/Adb";
import { Link } from "react-router-dom";
import { useAuth } from '../utils/UserContext';

function LoggedIn() {
  const { authUser, isLoggedIn } = useAuth();
  const style = {
    textDecoration: "none",
    color: "white",
  };
  
  const home = <Link to={`/home`} style={style}>Home</Link>
  const products = <Link to={`/products`} style={style}>Products</Link>
  const pricing = <Link to={`/pricing`} style={style}>Pricing</Link>
  const blog = <Link to={`/blog`} style={style}>Blog</Link>
  const about = <Link to={`/about`} style={style}>About</Link>
  const profile = <Link to={`/profile`} style={style}>Profile</Link>
  const account = <Link to={`/account`} style={style}>Account</Link>
  const dashboard = <Link to={`/dashboard`} style={style}>Dashboard</Link>
  const logout = <Link to={`/logout`} style={style}>Logout</Link>
  
  const pages = [home, products, pricing, blog, about];
  const settings = [home, profile, account, dashboard, logout];

  const [anchorElNav, setAnchorElNav] = React.useState(null);
  const [anchorElUser, setAnchorElUser] = React.useState(null);

  const handleOpenNavMenu = (event) => {
    setAnchorElNav(event.currentTarget);
  };
  const handleOpenUserMenu = (event) => {
    setAnchorElUser(event.currentTarget);
  };

  const handleCloseNavMenu = () => {
    setAnchorElNav(null);
  };

  const handleCloseUserMenu = () => {
    setAnchorElUser(null);
  };

  const [userData, setUserData] = useState('');

  const getUserData = async () => {
    if(!authUser) return;
    const res = await fetch(`http://localhost:8000/api/users/${authUser}`);
    const data = await res.json();
    const user_data = data.user; 
    setUserData(user_data)
  };

  useEffect(() => {
    getUserData();
  }, [authUser]);

  // 如果未登录,不渲染导航栏
  if(!isLoggedIn) return null;

  return (
    <AppBar position="static">
      <Container maxWidth="xl">
        <Toolbar disableGutters>
          <AdbIcon sx={{ display: { xs: "none", md: "flex" }, mr: 1 }} />
          <Typography
            variant="h6"
            noWrap
            component={Link}
            to="/blog"
            sx={{
              mr: 2,
              display: { xs: "none", md: "flex" },
              fontFamily: "monospace",
              fontWeight: 700,
              letterSpacing: ".3rem",
              color: "inherit",
              textDecoration: "none",
            }}
          >
            {userData.user_name}
          </Typography>

          <Box sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}>
            <IconButton
              size="large"
              aria-label="account of current user"
              aria-controls="menu-appbar"
              aria-haspopup="true"
              onClick={handleOpenNavMenu}
              color="inherit"
            >
              <MenuIcon />
            </IconButton>
            <Menu
              id="menu-appbar"
              anchorEl={anchorElNav}
              anchorOrigin={{
                vertical: "bottom",
                horizontal: "left",
              }}
              keepMounted
              transformOrigin={{
                vertical: "top",
                horizontal: "left",
              }}
              open={Boolean(anchorElNav)}
              onClose={handleCloseNavMenu}
              sx={{
                display: { xs: "block", md: "none" },
              }}
            >
              {pages.map((page) => (
                <MenuItem key={page} onClick={handleCloseNavMenu}>
                  <Typography textAlign="center">{page}</Typography>
                </MenuItem>
              ))}
            </Menu>
          </Box>
          <AdbIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} />
          <Typography
            variant="h5"
            noWrap
            component={Link}
            to="/"
            sx={{
              mr: 2,
              display: { xs: "flex", md: "none" },
              flexGrow: 1,
              fontFamily: "monospace",
              fontWeight: 700,
              letterSpacing: ".3rem",
              color: "inherit",
              textDecoration: "none",
            }}
          >
          </Typography>
          <Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}>
            {pages.map((page) => (
              <Button
                key={page}
                onClick={handleCloseNavMenu}
                sx={{ my: 2, color: "white", display: "block" }}
              >
                {page}
              </Button>
            ))}
          </Box>

          <Box sx={{ flexGrow: 0 }}>
            <Tooltip title="Open settings">
              <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
                <Avatar
                  alt={userData.user_name}
                  src={userData.profile_pic}
                />
              </IconButton>
            </Tooltip>
            <Menu
              sx={{ mt: "45px" }}
              id="menu-appbar"
              anchorEl={anchorElUser}
              anchorOrigin={{
                vertical: "top",
                horizontal: "right",
              }}
              keepMounted
              transformOrigin={{
                vertical: "top",
                horizontal: "right",
              }}
              open={Boolean(anchorElUser)}
              onClose={handleCloseUserMenu}
            >
              {settings.map((setting) => (
                <MenuItem key={setting} onClick={handleCloseUserMenu}>
                  <Typography textAlign="center">{setting}</Typography>
                </MenuItem>
              ))}
            </Menu>
          </Box>
        </Toolbar>
      </Container>
    </AppBar>
  );
}
export default LoggedIn;

PrivateRoute.js

import React from 'react'
import { Navigate } from 'react-router-dom'
import { useAuth } from '../utils/UserContext';

const PrivateRoute = ({ children }) => {
    const { isLoggedIn } = useAuth()

    return isLoggedIn ? children : <Navigate to='/' />
}

export default PrivateRoute;

关键修复点

  1. 状态持久化:在UserContext.js中添加两个useEffect,分别用于初始化时从localStorage读取登录状态,以及状态变化时保存到localStorage,避免页面刷新丢失登录状态。
  2. PrivateRoute使用方式修正:将PrivateRoute包裹在各个受保护路由的element中,确保访问这些路由时会验证登录状态;同时移除对LoggedIn的包裹,改为让LoggedIn根据isLoggedIn状态自行决定是否渲染。
  3. LoggedIn组件优化:
    • 通过useAuth直接获取authUser和isLoggedIn状态,无需通过props传递
    • 将原本的href改为Link组件,避免页面刷新导致状态丢失
    • 在getUserData的useEffect依赖中添加authUser,确保用户状态变化时重新获取用户数据
    • 添加未登录时的判断,不渲染导航栏
  4. PrivateRoute简化:移除不必要的cloneElement,直接根据登录状态返回子组件或重定向。

内容的提问来源于stack exchange,提问作者ShyRell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 20:51:59