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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 22:40:30