讲师中心 微信公众号
AI工具推荐 视频效率加速

React 应用中刷新页面后认证状态丢失的解决方案

秋雪酱_4818

秋雪酱_4818

发布时间:2025-07-19 13:44:01

|

253人浏览过

|

来源于php中文网

原创

React 应用中刷新页面后认证状态丢失的解决方案

本文旨在解决 React 应用中刷新页面后认证状态(如用户ID)丢失的问题。核心原因在于 React 组件状态在页面刷新时会重新初始化。通过利用 localStorage 实现数据持久化,并结合认证上下文(AuthContext)中的 loading 状态,确保在数据加载完成后才进行相关操作,从而维护用户认证状态的连续性。

问题分析:React 状态的生命周期与持久化挑战

在 react 应用中,当用户刷新页面时,整个应用会重新加载。这意味着所有组件的状态(通过 usestate 或 usereducer 管理)都会被重置为其初始值。对于认证信息(如用户id、令牌、角色),如果它们仅存储在组件或上下文的内存状态中,刷新后就会丢失,导致用户需要重新登录或应用无法识别其身份。

即使在自定义 Hook (如 useAuth) 中使用了 useEffect 来从 localStorage 读取数据,也存在一个时间差:

  1. 组件(包括 AuthProvider)首次渲染时,auth 状态会被初始化为 {}。
  2. AuthProvider 中的 useEffect 随后执行,异步地从 localStorage 读取数据并更新 auth 状态。
  3. 在此 useEffect 完成之前,任何依赖 auth 状态的子组件可能会在 auth.id 仍为 undefined 或 null 的情况下尝试访问它,从而导致错误或非预期行为。

在提供的代码示例中,Exercises 组件在 useEffect 和 onSubmit 中都直接使用了 auth.id。当页面刷新时,useAuth 钩子内部的 auth 状态会先被初始化为 {},尽管 AuthProvider 会尝试从 localStorage 加载数据,但 Exercises 组件可能在 auth.id 尚未被正确赋值之前就尝试访问它,导致 auth.id 为 null 或 undefined。

解决方案:利用 localStorage 与 loading 状态管理数据加载

为了解决这个问题,我们需要确保在认证数据从 localStorage 加载完成之前,依赖这些数据的组件不会进行操作。这可以通过在认证上下文中引入一个 loading 状态来管理。

1. 认证上下文 (AuthContext 和 AuthProvider) 的实现

AuthProvider 的职责是在应用启动时尝试从 localStorage 加载持久化的认证信息,并提供一个 loading 状态来指示数据是否已加载完成。

// AuthContext.js
import { createContext, useState, useEffect } from "react";

const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
  const [auth, setAuth] = useState({});
  const [loading, setLoading] = useState(true); // 初始为 true,表示正在加载

  useEffect(() => {
    // 尝试从 localStorage 读取认证信息
    const storedToken = localStorage.getItem("accessToken");
    const storedRoles = localStorage.getItem("roles");
    const storedId = localStorage.getItem("userId");

    if (storedToken && storedRoles && storedId) { // 确保所有关键信息都存在
      try {
        const token = JSON.parse(storedToken);
        const roles = JSON.parse(storedRoles);
        const id = JSON.parse(storedId);
        // 如果成功解析,则更新 auth 状态
        setAuth({ token, roles, id });
      } catch (e) {
        // 解析失败,可能是数据损坏,清空 localStorage
        console.error("Failed to parse stored auth data:", e);
        localStorage.removeItem("accessToken");
        localStorage.removeItem("roles");
        localStorage.removeItem("userId");
      }
    }
    setLoading(false); // 数据加载尝试完成后,设置 loading 为 false
  }, []); // 仅在组件挂载时运行一次

  // 假设还有一个 logout 函数,用于清除认证状态和 localStorage
  const logout = () => {
    setAuth({});
    localStorage.removeItem("accessToken");
    localStorage.removeItem("roles");
    localStorage.removeItem("userId");
  };

  return (
    <AuthContext.Provider value={{ auth, setAuth, loading, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

关键点:

  • loading 状态初始设置为 true。
  • useEffect 在组件挂载时运行一次,负责从 localStorage 读取数据。
  • 无论是否成功读取到数据,setLoading(false) 都会在 useEffect 结束时执行,表示数据加载过程已完成。
  • 添加了 try-catch 块来处理 JSON.parse 可能出现的错误,防止 localStorage 中存储了无效数据。

2. 在消费组件中等待 loading 状态

在任何需要访问 auth 信息的组件中(例如 Exercises 组件),都应该在 loading 状态为 false 之前避免使用 auth 对象中的敏感数据,如 auth.id。

Accessibility Review
Accessibility Review

对设计或页面进行 WCAG 2.1 AA 无障碍审计。触发关键词:“audit accessibility”、 “check a11y”、 “is this accessible?” 或在审查设计时使用。

下载
// ExercisePage.js
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import styles from "./ExercisePage.module.css";
import api from "../../apis/requestService";
import useAuth from "../../hooks/useAuth"; // 假设 useAuth 钩子返回 auth, setAuth, loading

function Exercises() {
  const { auth, loading } = useAuth(); // 获取 auth 和 loading 状态
  const { id } = useParams();
  const navigate = useNavigate();
  const [requests, setRequests] = useState([]);
  const [exerciseData, setExerciseData] = useState({
    weight: "",
    reps: "",
    exerciseId: id,
    date: null,
  });
  const [err, setErr] = useState("");
  const [popupStyle, showPopup] = useState("hide");

  const { weight, reps } = exerciseData;

  useEffect(() => {
    setExerciseData((prevData) => ({
      ...prevData,
      exerciseId: id,
      date: new Date(),
    }));

    // 只有当 loading 为 false 且 auth.id 存在时才发起请求
    if (!loading && auth.id) {
      api.getUserExercises(id).then((response) => {
        setRequests(response.data);
      }).catch(error => {
        console.error("Error fetching user exercises:", error);
        setErr("无法加载训练记录。");
      });
    }
  }, [id, auth.id, loading]); // 依赖中加入 loading

  const onInputChange = (e) => {
    setExerciseData({ ...exerciseData, [e.target.name]: e.target.value });
  };

  const onSubmit = (e) => {
    e.preventDefault();

    // 在提交前再次检查 auth.id 是否可用
    if (loading || !auth.id) {
      setErr("认证信息尚未加载或无效,请稍候再试或重新登录。");
      popup();
      return;
    }

    console.log("User id: " + auth.id);
    const updatedExerciseData = {
      ...exerciseData,
      userId: auth.id,
      date: new Date(),
    };

    api
      .createRequest(updatedExerciseData)
      .then((response) => {
        if (response.data.id) {
          // 创建成功后,重新获取用户训练记录
          return api.getUserExercises(auth.id);
        } else {
          throw new Error("创建请求时发生错误。");
        }
      })
      .then((response) => {
        setRequests(response.data);
        setExerciseData({ ...updatedExerciseData, weight: "", reps: "" }); // 清空输入
        setErr(""); // 清除错误信息
        popup(); // 显示成功提示
      })
      .catch((error) => {
        console.error(error);
        setErr("创建请求时发生错误:" + (error.response?.data?.message || error.message));
        popup();
      });
  };

  const popup = () => {
    showPopup("exercise-popup");
    setTimeout(() => showPopup("hide"), 3000);
  };

  // 如果正在加载认证信息,可以显示加载状态或不渲染依赖 auth 的部分
  if (loading) {
    return <div className={styles.wrapper}>加载认证信息...</div>;
  }

  // 如果 auth.id 不存在(例如未登录),可以重定向或显示提示
  if (!auth.id) {
    return <div className={styles.wrapper}>请登录以查看和设置训练。</div>;
    // 或者 navigate('/login');
  }

  return (
    <div className={styles.wrapper}>
      {/* ... 现有渲染逻辑 ... */}
      <div className={styles.content}>
        {requests.length > 0 ? (
          requests.map((request, index) => (
            <div key={index} className={styles.requestBox}>
              <div className={styles.requestDetails}>
                <h2>{request.exercise.name}</h2>
                <p>{request.exercise.description}</p>
              </div>
              <img
                src={request.exercise.imageUrl}
                alt={request.exercise.name}
              />
              <div className={styles.requestInfo}>
                <p>Weight: {request.weight} kg</p>
                <p>Reps: {request.reps}</p>
                <p>Date: {new Date(request.date).toLocaleDateString()}</p>
              </div>
            </div>
          ))
        ) : (
          <p>暂无训练记录。</p>
        )}
      </div>
      <form onSubmit={(e) => onSubmit(e)} className={styles.exerciseForm}>
        <h1 className={styles.h1Text}>
          设置 <br /> 训练
        </h1>
        <div className={styles.inputContainer}>
          <label htmlFor="weight" className={styles.inputLabel}>
            输入重量
          </label>
          <input
            id="weight"
            name="weight"
            type="number"
            value={weight}
            min="0"
            onChange={onInputChange}
            className={styles.inputBox}
          />
        </div>
        <div className={styles.inputContainer}>
          <label htmlFor="reps" className={styles.inputLabel}>
            输入次数
          </label>
          <input
            id="reps"
            name="reps"
            type="number"
            value={reps}
            min="0"
            onChange={onInputChange}
            className={styles.inputBox}
          />
        </div>
        <button className={styles.exerciseBtn} type="submit">
          +
        </button>
        <div className={popupStyle}>
          <h3>{err}</h3>
        </div>
      </form>
    </div>
  );
}

export default Exercises;

关键点:

  • 条件渲染: 在组件的最顶部,根据 loading 状态进行条件渲染。如果 loading 为 true,则显示一个加载提示。这可以防止在认证信息尚未加载完成时,组件尝试使用 auth.id。
  • useEffect 依赖: 将 loading 加入到 useEffect 的依赖数组中。这样,当 loading 状态从 true 变为 false 时,useEffect 会重新运行,确保在 auth.id 可用后才发起 getUserExercises 请求。
  • onSubmit 检查: 在表单提交处理函数中,再次检查 loading 状态和 auth.id 是否有效,以防止用户在数据未准备好时提交表单。

注意事项与最佳实践

  1. 数据敏感性与 localStorage 安全:

    • localStorage 中的数据是明文存储的,容易被客户端脚本访问。
    • 不建议直接在 localStorage 中存储敏感的认证令牌(如 JWT)。更安全的做法是使用 HttpOnly cookie,它们无法通过 JavaScript 访问,从而降低 XSS 攻击的风险。
    • 如果必须在 localStorage 存储,请考虑对数据进行加密,但这会增加复杂性。对于用户ID等非敏感信息,通常风险可控。
  2. JSON.parse 和 JSON.stringify:

    • localStorage 只能存储字符串。因此,在存储对象(如 auth 对象的一部分)时,需要使用 JSON.stringify() 将其转换为字符串;在读取时,使用 JSON.parse() 将其转换回对象。
    • 确保在 JSON.parse 时进行错误处理,以防存储的数据格式不正确。
  3. 用户体验:

    • 在 loading 期间显示加载指示器(Spinner),可以提升用户体验,告知用户应用正在后台处理。
    • 如果认证失败(例如 auth.id 为空),及时给出用户提示或重定向到登录页面。
  4. 其他持久化方案:

    • sessionStorage: 类似于 localStorage,但数据仅在当前浏览器会话中有效,关闭浏览器标签页或窗口后数据即被清除。
    • Redux Persist / Zustand Persist Middleware: 对于使用状态管理库(如 Redux、Zustand)的应用,可以使用其提供的持久化中间件,简化状态的存储和恢复过程。
    • 服务器端会话: 将会话信息存储在服务器端,客户端只存储一个会话ID(通常在 HttpOnly cookie 中),这是最安全的认证持久化方式之一。

通过以上改进,您的 React 应用将能够更稳健地管理用户认证状态,即使在页面刷新后也能保持用户登录状态,提供更流畅的用户体验。

热门AI工具

更多
讯飞智作

讯飞智作是一款AI视频创作工具,AI文本配音工具,数字人课程、营销视频制作。

Laper
Laper Hot

Laper是专为编剧、导演和制片人推出的 AI 原生剧本创作工具。

立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

Loomy
Loomy Hot

一款AI工具,主要用于科大讯飞发布的桌面级 AI 助理,比 OpenClaw 更易用、更安全!,适合需要提升相关任务效率的用户。

AionClaw
AionClaw Hot

AionClaw是一款面向办公、创作和编程任务的AI桌面智能体。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

PixPix
PixPix Hot

PixPix是一款面向电商视觉生产的AI商品图生成工具。

相关专题

更多
什么是中间件
什么是中间件

中间件是一种软件组件,充当不兼容组件之间的桥梁,提供额外服务,例如集成异构系统、提供常用服务、提高应用程序性能,以及简化应用程序开发。想了解更多中间件的相关内容,可以阅读本专题下面的文章。

649

2024.05.11

Golang 中间件开发与微服务架构
Golang 中间件开发与微服务架构

本专题系统讲解 Golang 在微服务架构中的中间件开发,包括日志处理、限流与熔断、认证与授权、服务监控、API 网关设计等常见中间件功能的实现。通过实战项目,帮助开发者理解如何使用 Go 编写高效、可扩展的中间件组件,并在微服务环境中进行灵活部署与管理。

624

2025.12.18

ThinkPHP中间件机制与请求拦截处理实践
ThinkPHP中间件机制与请求拦截处理实践

本专题围绕 ThinkPHP 中间件体系展开,深入讲解中间件的定义、注册与执行流程。内容包括全局中间件与路由中间件的区别、请求前后处理逻辑、自定义中间件开发以及权限验证与日志处理应用。通过实际案例,帮助开发者掌握中间件在项目中的核心作用与最佳实践。

438

2026.03.31

json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

2155

2023.08.07

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

3342

2023.08.23

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

1116

2023.10.13

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

3839

2025.09.10

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

549

2023.09.22

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

0

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Sass 教程
Sass 教程

共14课时 | 1.5万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 6万人学习

CSS教程
CSS教程

共754课时 | 98.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn