背景
项目上有个更改时区的全局组件,同时还有一个可以更改时区的局部组件,想让更改时区的时候能联动起来,实时响应起来。
其实每次设置完时区的数据之后是存在了前端的 localStorage 里边,时���组件里边也是从 localStorage 拿去默认值来回显。如果当前页面不刷新,那么时间组件就不能更新到最新的 localStorage 数据。
怎么才能让 localStorage 存储的数也变成响应式呢?
实现
-
应该写个公共的方法,不仅仅时区数据能用,万一后边其他数据也能用。
-
项目是 React 项目,那就写个 hook
-
怎么才能让 localStorage 数据变成响应式呢?监听?
失败的案例 1
首先想到的是按照下边这种方式做,
useEffect(()=>{ console.log(11111, localStorage.getItem('timezone'))
},[localStorage.getItem('timezone')])
得到的测试结果肯定是失败的,但是为啥失败?我们也应该知道一下。查了资料说,使用 localStorage.getItem('timezone')
作为依赖项会导致每次渲染都重新计算依赖项,这不是正确的做法。
具体看一下官方文档:useEffect(setup, dependencies?)
在此说一下第二个参数 dependencies:
可选 dependencies
:setup
代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React[1],那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3]
这样内联编写。React 将使用 `Object.is`[2] 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。
-
如果你的一些依赖项是组件内部定义的对象或函数,则存在这样的风险,即它们将 导致 Effect 过多地重新运行。要解决这个问题,请删除不必要的 对象[3] 和 函数[4] 依赖项。你还可以 抽离状态更新[5] 和 非响应式的逻辑[6] 到 Effect 之外。
如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后重新连接,因为 createOptions
函数 在每次渲染时都不同[7]:
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
时,其他文档会收到通知。
写代码...
// 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;
使用方法:
// 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 变更,没法监听同一个页面的变更。
成功的案例
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_keyif (event.detail.key === localStorage_key) {// 更新状态变量 storageValueconst 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 数据统一封装,
// 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;
具体的业务页面组件中使用,
// 页面中
// ...
import useTimezone from "@/hooks/useTimezone";export default (props) => {// ...const [TimeZone] = useTimezone();useEffect(()=>{ console.log(11111, TimeZone) },[TimeZone)
}
测试结果必须是成功的啊!!!
小结
其实想要做到该效果,用全局 store 状态管理也能做到,条条大路通罗马嘛!不过本次需求由于历史原因一直使用的是 localStorage ,索性就想着 如何让 localStorage 存储变为响应式 ?
最后给大家推荐个好玩的小程序吧,在短视频时代,去水印小程序成为了用户的热门选择。它能快速、轻松地去除视频中的水印,让你分享更干净的内容。只需上传视频,选择去水印功能,几秒钟后,清晰的画面便呈现在眼前。无论是制作个人视频还是分享精彩瞬间,这款小程序都能帮助你提升视频质量,吸引更多观众。告别水印,让你的创作更加自由,尽情展现你的才华吧!(小程序搜索:去水印AI助手)