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

React不同路由刷新避免数据丢失:示例问题解析

路由刷新保持状态问题解析

问题描述

我希望在不同路由路径下刷新页面时避免丢失状态,好奇为何第一个示例无法生效。据我理解,组件挂载时先渲染自身,再执行useEffect。这里有3个useEffect:第一个获取数据并保存到invoiceList状态,第二个本应将invoiceList状态数据存入localStorage,第三个用于读取数据。但第一个示例中localStorage的"invoiceData"键被存入空数组,明明第一个useEffect后invoiceList已有数据,这是为什么?我提供的第二个示例可行,我移除了第二个useEffect,直接在第一个useEffect中用fetch返回的数据设置localStorage。同时我想确认当前实现是否正确,欢迎任何反馈。


第一个示例(无效)

import { ReactElement, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";

type Props = {};

const Root = (props: Props): ReactElement => {
  const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch("./data.json");
      const data = await response.json();

      setInvoiceList(data);
    };

    fetchData();
  }, []);

  useEffect(() => {
    window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList));
  }, []);

  useEffect(() => {
    setInvoiceList(
      JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
    );
  }, []);

  return (
    <div>
      <Bar />
      <Outlet context={{ invoiceList }} />
    </div>
  );
};

export default Root;

第二个示例(有效)

import { ReactElement, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Bar } from "../../components/Bar/Bar";
import { Invoice } from "./Root.utils";

type Props = {};

const Root = (props: Props): ReactElement => {
  const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch("./data.json");
      const data = await response.json();

      window.localStorage.setItem("invoiceData", JSON.stringify(data));

      setInvoiceList(data);
    };

    fetchData();
  }, []);

  useEffect(() => {
    setInvoiceList(
      JSON.parse(window.localStorage.getItem("invoiceData") || "[]")
    );
  }, []);

  return (
    <div>
      <Bar />
      <Outlet context={{ invoiceList }} />
    </div>
  );
};

export default Root;

问题原因与解析

第一个示例失效的核心原因

三个useEffect都使用了空依赖数组[],意味着它们会在组件首次挂载后同步执行,执行顺序和代码书写顺序一致:

  1. 第一个useEffect触发异步fetch请求,此时请求还未完成,setInvoiceList(data)不会立即执行,invoiceList仍保持初始值[]。
  2. 第二个useEffect紧接着执行,此时invoiceList还是空数组,所以存入localStorage的是[]。
  3. 第三个useEffect执行,读取localStorage里的空数组并设置到invoiceList。
  4. 后续fetch请求完成,调用setInvoiceList(data)更新状态,但三个useEffect因依赖为空不会再次执行,新的invoiceList数据无法存入localStorage。刷新页面后,第三个useEffect读取的还是之前存入的空数组,导致状态丢失。

第二个示例有效的原因

把存入localStorage的逻辑放到了fetch请求成功的回调里,直接用请求返回的data写入localStorage,避免了依赖状态更新的延迟问题:

  1. fetch请求完成后,先把原始数据存入localStorage,再更新组件状态。
  2. 即使第三个useEffect在挂载时执行,此时localStorage已经有了有效数据,能正确设置状态;刷新页面时,也能直接读取之前存储的数据恢复状态。

优化建议

  1. 初始状态直接从localStorage读取,避免组件初始渲染时显示空数组:
const [invoiceList, setInvoiceList] = useState<Invoice[]>(() => {
  try {
    const storedData = window.localStorage.getItem("invoiceData");
    return storedData ? JSON.parse(storedData) : [];
  } catch (e) {
    console.error("读取localStorage失败", e);
    return [];
  }
});
  1. 避免重复设置状态:如果localStorage已有数据,可以跳过fetch请求(根据业务需求调整),减少不必要的网络请求:
useEffect(() => {
  const fetchData = async () => {
    const storedData = window.localStorage.getItem("invoiceData");
    if (storedData) {
      setInvoiceList(JSON.parse(storedData));
      return;
    }
    const response = await fetch("./data.json");
    const data = await response.json();
    window.localStorage.setItem("invoiceData", JSON.stringify(data));
    setInvoiceList(data);
  };
  fetchData();
}, []);
  1. 添加状态更新时的自动存储:如果后续invoiceList会被修改,需要监听状态变化,自动同步到localStorage:
useEffect(() => {
  window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList));
}, [invoiceList]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 16:10:30