DEV Community

Cover image for How to Make localStorage Data Reactive
Rain Zhang
Rain Zhang

Posted on

How to Make localStorage Data Reactive

Major Update

📣 Exciting News Alert! Don't miss out!

Infini Tech is dedicated to providing high-quality open-source tools to developers and enterprises, continuously enhancing the vibrancy of the tech ecosystem. Along with maintaining popular projects like the analysis-ik and analysis-pinyin plugins, we are actively driving the release of more top-tier open-source products.

To celebrate Infini Tech’s third anniversary, the following products and tools are now fully open-source:

  • INFINI Framework
  • INFINI Gateway
  • INFINI Console
  • INFINI Agent
  • INFINI Loadgen
  • INFINI Coco AI

All these open-source projects are available on GitHub: https://github.com/infinilabs

We’d greatly appreciate your Star🌟 to support us!

Background

While developing the company's project INFINI Cloud (not yet open-source, stay tuned!), there was a global timezone adjustment component and a local timezone adjustment component. The goal was to have them synchronize and respond in real-time when timezone changes occurred.

Image description

Tip: If you're interested in this time component, feel free to visit https://github.com/infinilabs/ui-common. We’d love your Star🌟 and collaboration.

The timezone data was stored in the frontend's localStorage, and the time component fetched its default value from there. However, if the current page wasn’t refreshed, the time component couldn't update to the latest localStorage data.

How can we make localStorage reactive?

Implementation

  1. Create a reusable method, applicable to timezone and other data in the future.
  2. As the project is React-based, write a custom hook.
  3. To make localStorage reactive, should we rely on listeners?

Failed Attempt 1

Initially, the idea was to approach it as follows:

useEffect(() => { 
    console.log(11111, localStorage.getItem('timezone')); 
}, [localStorage.getItem('timezone')]);
Enter fullscreen mode Exit fullscreen mode

This approach failed. Why? Research indicates that using localStorage.getItem('timezone') as a dependency causes recalculations on every render, which is incorrect.

For details, refer to the official React documentation on useEffect(setup, dependencies?).

Failed Attempt 2

The next idea was to use window's storage event to listen for changes:

// 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;
Enter fullscreen mode Exit fullscreen mode

Testing showed no effect because the storage event only listens for changes across different pages of the same origin, not changes within the same page.

Successful Approach

The solution involves creating a custom event:

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;
Enter fullscreen mode Exit fullscreen mode

Integration and Testing

Encapsulate timezone logic in a 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;
Enter fullscreen mode Exit fullscreen mode

Use it in your component:

import useTimezone from "@/hooks/useTimezone";

export default (props) => {
  const [TimeZone] = useTimezone();

  useEffect(() => { 
    console.log(11111, TimeZone); 
  }, [TimeZone]);
};
Enter fullscreen mode Exit fullscreen mode

The tests were successful!

Conclusion

While a global store or state management solution could achieve similar results, this implementation leverages localStorage for historical reasons.

Have better ideas? Let’s discuss!

Top comments (1)

Collapse
 
alvarogfn profile image
Alvaro Guimarães

Cool, I like this approach to making localStorage reactive. I would love to see this approach with indexeddb, with libraries like dexie.org/ to make offline-first applications.