--- title: "如何让 localStorage 数据实现实时响应" date: 2024-12-09 lastmod: 2024-12-09 description: "极限科技开源多款产品,包括INFINI Framework、Gateway、Console等。文章探讨如何使localStorage数据响应式,通过自定义Hook实现监听与更新,解决React项目中时区组件联动问题。" tags: ["localStorage", "响应式", "时间组件", "时区组件", "React", "Web 开发"] summary: "重大事项 # 📣 :重大事项提前通知!快来围观,不容错过! 极限科技一直致力于为开发者和企业提供优质的开源工具,提升整个技术生态的活力。除了维护国内最流行的分词器 analysis-ik 和 analysis-pinyin,也在不断推动更多高质量开源产品的诞生。 在极限科技成立三周年之际,公司宣布以下产品和工具已全面开源: INFINI Framework https://github.com/infinilabs/framework INFINI Gateway https://github.com/infinilabs/gateway INFINI Console https://github.com/infinilabs/console INFINI Agent https://github.com/infinilabs/agent INFINI Loadgen https://github.com/infinilabs/loadgen INFINI Coco AI https://github.com/infinilabs/coco-app 以上开源软件都可以在 Github 上面找到: https://github.com/infinilabs 希望大家都能给个免费的 Star🌟 支持一下!!! 背景 # 在开发公司项目 INFINI Cloud(暂未开源,敬请期待。 不过此次开源的同类项目有 INFINI Console)的时候,该项目上有个更改时区的全局组件,同时还有一个可以更改时区的局部组件,想让更改时区的时候能联动起来,实时响应起来。 Tip:如果有人对该时间组件感兴趣,可以移步 https://github.com/infinilabs/ui-common,同时也希望收到您 Star🌟 支持,也希望和大家一起共建。 其实每次设置完时区的数据之后是存在了前端的 localStorage 里边,时间组件里边也是从 localStorage 拿去默认值来回显。如果当前页面不刷新,那么时间组件就不能更新到最新的 localStorage 数据。 怎么才能让 localStorage 存储的数也变成响应式呢? 实现 # 应该写个公共的方法,不仅仅时区数据能用,万一后边其他数据也能用。 项目是 React 项目,那就写个 hook 怎么才能让 localStorage 数据变成响应式呢?监听? 失败的案例 1 # 首先想到的是按照下边这种方式做," --- ## 重大事项 📣 :**重大事项提前通知!快来围观,不容错过!** **极限科技**一直致力于为开发者和企业提供优质的开源工具,提升整个技术生态的活力。除了维护国内最流行的分词器 `analysis-ik` 和 `analysis-pinyin`,也在不断推动更多高质量开源产品的诞生。 在极限科技成立三周年之际,公司宣布以下产品和工具已全面开源: - INFINI Framework https://github.com/infinilabs/framework - INFINI Gateway https://github.com/infinilabs/gateway - INFINI Console https://github.com/infinilabs/console - INFINI Agent https://github.com/infinilabs/agent - INFINI Loadgen https://github.com/infinilabs/loadgen - INFINI Coco AI https://github.com/infinilabs/coco-app 以上开源软件都可以在 Github 上面找到: https://github.com/infinilabs 希望大家都能给个免费的 **Star🌟** 支持一下!!! ## 背景 在开发公司项目 `INFINI Cloud`(暂未开源,敬请期待。 不过此次开源的同类项目有 `INFINI Console`)的时候,该项目上有个**更改时区的全局组件**,同时还有一个**可以更改时区的局部组件**,想让更改时区的时候能**联动起来,实时响应起来**。 ![image.png](/img/blog/2024/rain/localStorage.jpg) >Tip:如果有人对该时间组件感兴趣,可以移步 https://github.com/infinilabs/ui-common,同时也希望收到您 **Star🌟** 支持,也希望和大家一起共建。 其实每次设置完时区的数据之后是存在了前端的 **localStorage** 里边,时间组件里边也是从 **localStorage** 拿去默认值来回显。如果当前页面不刷新,那么时间组件就不能更新到最新的 **localStorage** 数据。 **怎么才能让 localStorage 存储的数也变成响应式呢?** ## 实现 1. 应该写个公共的方法,不仅仅时区数据能用,万一后边其他数据也能用。 2. 项目是 React 项目,那就写个 hook 3. 怎么才能让 localStorage 数据变成响应式呢?监听? ### 失败的案例 1 首先想到的是按照下边这种方式做, ```js useEffect(()=>{ console.log(11111, localStorage.getItem('timezone')) },[localStorage.getItem('timezone')]) ``` 得到的测试结果肯定是失败的,但是为啥失败?我们也应该知道一下。查了资料说,**使用 `localStorage.getItem('timezone')` 作为依赖项会导致每次渲染都重新计算依赖项,这不是正确的做法。** 具体看一下官方文档:`useEffect(setup, dependencies?)`  在此说一下第二个参数 **dependencies**: **可选** `dependencies`:`setup` 代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 [配置了 React](https://zh-hans.react.dev/learn/editor-setup#linting),那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 `[dep1, dep2, dep3]` 这样内联编写。React 将使用 [`Object.is`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。 - 如果你的一些依赖项是组件内部定义的对象或函数,则存在这样的风险,即它们将 **导致 Effect 过多地重新运行**。要解决这个问题,请删除不必要的 [对象](https://zh-hans.react.dev/reference/react/useEffect#removing-unnecessary-object-dependencies) 和 [函数](https://zh-hans.react.dev/reference/react/useEffect#removing-unnecessary-function-dependencies) 依赖项。你还可以 [抽离状态更新](https://zh-hans.react.dev/reference/react/useEffect#updating-state-based-on-previous-state-from-an-effect) 和 [非响应式的逻辑](https://zh-hans.react.dev/reference/react/useEffect#reading-the-latest-props-and-state-from-an-effect) 到 Effect 之外。 如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后重新连接,因为 `createOptions` 函数 [在每次渲染时都不同](https://zh-hans.react.dev/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally): ```js function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); function createOptions() { // 🚩 此函数在每次重新渲染都从头开始创建 return { serverUrl: serverUrl, roomId: roomId }; } useEffect(() => { const options = createOptions(); // 它在 Effect 中被使用 const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, [createOptions]); // 🚩 因此,此依赖项在每次重新渲染都是不同的 // ... } ``` ### 失败的案例 2 一开始能想到的是监听,那就用 window 上监听事件。 在 React 应用中监听 `localStorage` 的变化,可以使用 `window` 对象的 `storage` 事件。这个事件在同一域名的不同文档之间共享,当某个文档修改 `localStorage` 时,其他文档会收到通知。 写代码... ```js // useRefreshLocalStorage.js import { useState, useEffect } from 'react'; const useRefreshLocalStorage = (key) => { const [storageValue, setStorageValue] = useState( localStorage.getItem(key) ); useEffect(() => { const handleStorageChange = (event) => { if (event.key === key) { setStorageValue(event.newValue) } }; window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); }; }, [key]); return [storageValue]; }; export default useRefreshLocalStorage; ``` 使用方式: ```js // useTimezone.js import { useState, useEffect } from "react"; import { getTimezone, timezoneKey } from "@/utils/utils"; import useRefreshLocalStorage from "./useRefreshLocalStorage"; function useTimezone() { const [TimeZone, setTimeZone] = useState(() => getTimezone()); const [storageValue] = useRefreshLocalStorage(timezoneKey); useEffect(() => { setTimeZone(() => getTimezone()); }, [storageValue]); return [TimeZone]; } export default useTimezone; ``` 经过测试,失败了,没有效果!!!那到底怎么回事呢?哪里出现问题了?查阅资料经过思考,可能出现的问题的原因有:**只能监听同源的两个页面之间的 storage 变更,没法监听同一个页面的变更。** ### 成功的案例 ```js import { useState, useEffect } from "react"; // 自定义 Hook,用于监听 localStorage 中指定键的变化 function useRefreshLocalStorage(localStorage_key) { // 检查 localStorage_key 是否有效 if (!localStorage_key || typeof localStorage_key !== "string") { return [null]; } // 创建一个状态变量来保存 localStorage 中的值 const [storageValue, setStorageValue] = useState( localStorage.getItem(localStorage_key) ); useEffect(() => { // 保存原始的 localStorage.setItem 方法 const originalSetItem = localStorage.setItem; // 重写 localStorage.setItem 方法,添加事件触发逻辑 localStorage.setItem = function(key, newValue) { // 创建一个自定义事件,用于通知 localStorage 的变化 const setItemEvent = new CustomEvent("setItemEvent", { detail: { key, newValue }, }); // 触发自定义事件 window.dispatchEvent(setItemEvent); // 调用原始的 localStorage.setItem 方法 originalSetItem.apply(this, [key, newValue]); }; // 事件处理函数,用于处理自定义事件 const handleSetItemEvent = (event) => { const customEvent = event; // 检查事件的键是否是我们关心的 localStorage_key if (event.detail.key === localStorage_key) { // 更新状态变量 storageValue const updatedValue = customEvent.detail.newValue; setStorageValue(updatedValue); } }; // 添加自定义事件的监听器 window.addEventListener("setItemEvent", handleSetItemEvent); // 清除事件监听器和还原原始方法 return () => { // 移除自定义事件监听器 window.removeEventListener("setItemEvent", handleSetItemEvent); // 还原原始的 localStorage.setItem 方法 localStorage.setItem = originalSetItem; }; // 依赖数组,只在 localStorage_key 变化时重新运行 useEffect }, [localStorage_key]); // 返回当前的 storageValue // 为啥没有返回 setStorageValue ? // 因为想让用户直接操作自己真实的 “setValue” 方法,这里只做一个只读。 return [storageValue]; } export default useRefreshLocalStorage; ``` 具体的实现步骤如上,每一步也加上了注释。 接下来就是测试了, useTimezone 针对 timezone 数据统一封装, ```js // useTimezone.js import { useState, useEffect } from "react"; import { getTimezone, timezoneKey } from "@/utils/utils"; import useRefreshLocalStorage from "./useRefreshLocalStorage"; function useTimezone() { const [TimeZone, setTimeZone] = useState(() => getTimezone()); const [storageValue] = useRefreshLocalStorage(timezoneKey); useEffect(() => { setTimeZone(() => getTimezone()); }, [storageValue]); return [TimeZone]; } export default useTimezone; ``` 具体的业务页面组件中使用, ```js // 页面中 // ... import useTimezone from "@/hooks/useTimezone"; export default (props) => { // ... const [TimeZone] = useTimezone(); useEffect(()=>{ console.log(11111, TimeZone) },[TimeZone) } ``` 测试结果必须是成功的啊!!! ## 小结 其实想要做到该效果,用全局 store 状态管理也能做到,条条大路通罗马嘛!不过本次需求由于历史原因一直使用的是 **localStorage** ,索性就想着 **如何让 localStorage 存储变为响应式 ?** 不知道大家还有什么更好的方法吗?