如何在Next.js应用中通过useContext持久化用户状态
问题描述
我正在开发一个基于React和Next.js的Web应用,后端为独立的Node.js API。登录时向API提交数据获取JWT,登录流程正常,但跳转至受保护的dashboard路由后,或刷新页面时用户上下文会丢失。
受保护路由代码
import React, {useContext, useEffect} from 'react' import { useRouter } from "next/router"; import { Context } from '../../context/context'; export default function DashboardIndexView() { const router = useRouter(); const {isUserAuthenticated, setUserToken, userToken} = useContext(Context); useEffect(() => { isUserAuthenticated() ? router.push("/dashboard") : router.push("/authentication/admin"); }, []); return ( <> <h1>Dashboard index view</h1> </> ) }
上下文文件代码
import React, {createContext, useEffect, useState} from 'react' import { useRouter } from "next/router"; import axios from 'axios' export const Context = createContext(null) const devURL = "http://localhost:4444/api/v1/" export const ContextProvider = ({children}) => { const router = useRouter() const [user, setUser] = useState() const [userToken, setUserToken] = useState() const [loading, setLoading] = useState(false) const [successMessage, setSuccessMessage] = useState("") const [errorMessage, setErrorMessage] = useState("") const Login = (em,pass) => { setLoading(true) axios.post(devURL+"authentication/login", { email : em, password : pass }) .then((res)=>{ setSuccessMessage(res.data.message) setErrorMessage(null) setUser(res.data.user) setUserToken(res.data.token) localStorage.setItem('userToken', res.data.token) localStorage.setItem('user', res.data.user) setLoading(false) }) .catch((err)=>{ setErrorMessage(err.response.data.message) setSuccessMessage(null) setLoading(false) }) } const Logout = () => { setUserToken() setUser() localStorage.setItem('userToken', null) localStorage.setItem('user', null) router.push('/authentication/admin') } const isUserAuthenticated = () => !!userToken return ( <Context.Provider value={{ Login, user, loading, userToken, setUserToken, Logout, successMessage, setSuccessMessage, setErrorMessage, isUserAuthenticated, errorMessage}}> {children} </Context.Provider> ) }
解决方案
问题核心是页面刷新时React状态重置,仅在登录时将token和用户信息存入localStorage,但未在应用初始化时从localStorage恢复状态。同时受保护路由逻辑存在跳转循环问题,需同步修正。
1. 从localStorage恢复状态
在ContextProvider中添加useEffect,初始化时读取localStorage中的token和用户信息并同步到state:
useEffect(() => { // 读取本地存储的数据 const savedToken = localStorage.getItem('userToken'); const savedUser = localStorage.getItem('user'); // 验证数据有效性后更新状态 if (savedToken && savedToken !== 'null') { setUserToken(savedToken); } if (savedUser && savedUser !== 'null') { // 注意:localStorage仅支持字符串存储,需解析为对象 setUser(JSON.parse(savedUser)); } }, []);
补充:登录时存储用户信息到
localStorage需转为字符串,修正Login函数中的对应行:localStorage.setItem('user', JSON.stringify(res.data.user))
2. 修正Logout函数
原代码将localStorage值设为null,改为直接移除对应项,避免后续读取时的判断异常:
const Logout = () => { setUserToken(null); setUser(null); localStorage.removeItem('userToken'); localStorage.removeItem('user'); router.push('/authentication/admin'); }
3. 修复受保护路由的跳转逻辑
当前代码会触发不必要的循环跳转,改为仅在未认证时跳转至登录页:
useEffect(() => { if (!isUserAuthenticated()) { router.push("/authentication/admin"); } }, [isUserAuthenticated, router]);
进阶:抽离路由保护逻辑为高阶组件
为了复用路由保护逻辑,创建withAuth高阶组件:
// components/withAuth.js import React, { useContext, useEffect } from 'react'; import { useRouter } from 'next/router'; import { Context } from '../context/context'; const withAuth = (WrappedComponent) => { const AuthComponent = (props) => { const router = useRouter(); const { isUserAuthenticated } = useContext(Context); useEffect(() => { if (!isUserAuthenticated()) { router.push('/authentication/admin'); } }, [isUserAuthenticated, router]); // 未认证时返回null避免渲染内容 return isUserAuthenticated() ? <WrappedComponent {...props} /> : null; }; return AuthComponent; }; export default withAuth;
在DashboardIndexView中使用该组件:
import withAuth from '../components/withAuth'; function DashboardIndexView() { return ( <> <h1>Dashboard index view</h1> </> ); } export default withAuth(DashboardIndexView);
4. 可选:增强安全性——验证token有效性
为防止过期或篡改的token被使用,初始化时可调用API验证token有效性:
useEffect(() => { const savedToken = localStorage.getItem('userToken'); const savedUser = localStorage.getItem('user'); if (savedToken && savedToken !== 'null') { // 调用后端验证接口 axios.get(devURL + 'authentication/verify-token', { headers: { Authorization: `Bearer ${savedToken}` } }) .then(res => { setUserToken(savedToken); setUser(JSON.parse(savedUser)); }) .catch(err => { // token无效时清除本地存储并跳转登录页 localStorage.removeItem('userToken'); localStorage.removeItem('user'); router.push('/authentication/admin'); }); } }, []);
内容的提问来源于stack exchange,提问作者EL-Mehdi Loukach
相关产品推荐
相关产品推荐

