如何让 localStorage 存储变为响应式

背景

项目上有个更改时区的全局组件 ,同时还有一个可以更改时区的局部组件 ,想让更改时区的时候能联动起来,实时响应起来

其实每次设置完时区的数据之后是存在了前端的 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

可选 dependenciessetup 代码中引用的所���响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3] 这样内联编写。React 将使用 Object.is 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。

  • 如果你的一些依赖项是组件内部定义的对象或函数,则存在这样的风险,即它们将 导致 Effect 过多地重新运行 。要解决这个问题,请删除不必要的 对象函数 依赖项。你还可以 抽离状态更新非响应式的逻辑 到 Effect 之外。

如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后重新连接,因为 createOptions 函数 在每次渲染时都不同

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 存储变为响应式 ?

不知道大家还有什么更好的方法吗?

相关推荐
崔庆才丨静觅10 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax