如何在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;
关键修复点
- 状态持久化:在
UserContext.js中添加两个useEffect,分别用于初始化时从localStorage读取登录状态,以及状态变化时保存到localStorage,避免页面刷新丢失登录状态。 - PrivateRoute使用方式修正:将
PrivateRoute包裹在各个受保护路由的element中,确保访问这些路由时会验证登录状态;同时移除对LoggedIn的包裹,改为让LoggedIn根据isLoggedIn状态自行决定是否渲染。 - LoggedIn组件优化:
- 通过
useAuth直接获取authUser和isLoggedIn状态,无需通过props传递 - 将原本的
href改为Link组件,避免页面刷新导致状态丢失 - 在
getUserData的useEffect依赖中添加authUser,确保用户状态变化时重新获取用户数据 - 添加未登录时的判断,不渲染导航栏
- 通过
- PrivateRoute简化:移除不必要的
cloneElement,直接根据登录状态返回子组件或重定向。
内容的提问来源于stack exchange,提问作者ShyRell
相关产品推荐
相关产品推荐

