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.
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
- Create a reusable method, applicable to timezone and other data in the future.
- As the project is React-based, write a custom hook.
- 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')]);
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;
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;
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;
Use it in your component:
import useTimezone from "@/hooks/useTimezone";
export default (props) => {
const [TimeZone] = useTimezone();
useEffect(() => {
console.log(11111, TimeZone);
}, [TimeZone]);
};
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)
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.