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都使用了空依赖数组[],意味着它们会在组件首次挂载后同步执行,执行顺序和代码书写顺序一致:
- 第一个
useEffect触发异步fetch请求,此时请求还未完成,setInvoiceList(data)不会立即执行,invoiceList仍保持初始值[]。 - 第二个
useEffect紧接着执行,此时invoiceList还是空数组,所以存入localStorage的是[]。 - 第三个
useEffect执行,读取localStorage里的空数组并设置到invoiceList。 - 后续
fetch请求完成,调用setInvoiceList(data)更新状态,但三个useEffect因依赖为空不会再次执行,新的invoiceList数据无法存入localStorage。刷新页面后,第三个useEffect读取的还是之前存入的空数组,导致状态丢失。
第二个示例有效的原因
把存入localStorage的逻辑放到了fetch请求成功的回调里,直接用请求返回的data写入localStorage,避免了依赖状态更新的延迟问题:
fetch请求完成后,先把原始数据存入localStorage,再更新组件状态。- 即使第三个
useEffect在挂载时执行,此时localStorage已经有了有效数据,能正确设置状态;刷新页面时,也能直接读取之前存储的数据恢复状态。
优化建议
- 初始状态直接从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 []; } });
- 避免重复设置状态:如果
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(); }, []);
- 添加状态更新时的自动存储:如果后续
invoiceList会被修改,需要监听状态变化,自动同步到localStorage:
useEffect(() => { window.localStorage.setItem("invoiceData", JSON.stringify(invoiceList)); }, [invoiceList]);
内容的提问来源于stack exchange,提问作者Adrian
相关产品推荐
相关产品推荐

