响应式监听 localStorage 存储?封装个自定义 Hook 不就好了!
背景
项目中有一个全局更改时区的组件,同时还有一个局部更改时区的组件。需求是当更改时区时,所有相关组件能够实时联动并更新。
其实每次设置完时区的数据之后是存储在前端的 localStorage
中,组件也是从 localStorage
中获取默认值并显示。如果当前页面不刷新,时间组件就无法更新到最新的 localStorage
数据。那么,如何让 localStorage
存储的数据变成响应式呢?
实现
为了解决这一问题,我们可以编写一个公共的自定义 Hook,这样不仅仅是时区数据,其他需要监听 localStorage
变化的数据也可以复用这一逻辑。
失败的案例 1
一开始尝试使用 useEffect
监听 localStorage
变化:
useEffect(()=>{
console.log(11111, localStorage.getItem('timezone'));
},[localStorage.getItem('timezone')])
测试结果失败。原因是 localStorage.getItem('timezone')
在每次渲染时都会重新计算,导致无法正确监听到变化。
失败的案例 2
接下来尝试使用 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;
然而,这种方式也失败了,因为 storage
事件只能监听同源的两个页面之间的 localStorage
变化,无法监听同一个页面的变化。
成功的案例
最终的解决方案是重写 localStorage.setItem
方法,通过触发自定义事件来实现响应式监听。
import { useState, useEffect } from "react";
function useRefreshLocalStorage(localStorage_key) {
if (!localStorage_key || typeof localStorage_key !== "string") {
return [null];
}
const [storageValue, setStorageValue] = useState(localStorage.getItem(localStorage_key));
useEffect(() => {
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, newValue) {
const setItemEvent = new CustomEvent("setItemEvent", { detail: { key, newValue } });
window.dispatchEvent(setItemEvent);
originalSetItem.apply(this, [key, newValue]);
};
const handleSetItemEvent = (event) => {
if (event.detail.key === localStorage_key) {
setStorageValue(event.detail.newValue);
}
};
window.addEventListener("setItemEvent", handleSetItemEvent);
return () => {
window.removeEventListener("setItemEvent", handleSetItemEvent);
localStorage.setItem = originalSetItem;
};
}, [localStorage_key]);
return [storageValue];
}
export default useRefreshLocalStorage;
使用示例
封装一个用于管理时区数据的自定义 Hook:
// 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
来存储数据,因此我们通过封装自定义 Hook 实现了 localStorage
数据的响应式更新。