@supunlakmal/hooks
Version:
A collection of reusable React hooks
103 lines • 4.7 kB
JavaScript
import { useState, useEffect, useCallback, useRef } from 'react';
// Helper function to safely parse JSON
function safeJsonParse(value) {
if (value === null)
return null;
try {
return JSON.parse(value);
}
catch (e) {
console.error('Error parsing JSON from localStorage', e);
return null; // Return null or a default value if parsing fails
}
}
// Helper to check if running in a browser environment
const isBrowser = typeof window !== 'undefined';
/**
* A hook similar to useState that persists the value in localStorage
* and syncs its state across multiple tabs/windows using the same key via the 'storage' event.
*
* @template T The type of the value to store.
* @param {string} key The key to use in localStorage.
* @param {T | (() => T)} initialValue The initial value or a function to compute it.
* @returns {[T, React.Dispatch<React.SetStateAction<T>>]} A stateful value and a function to update it.
*/
export function useSyncedLocalStorage(key, initialValue) {
// Resolve the initial value once
const resolvedInitialValue = useRef(typeof initialValue === 'function'
? initialValue()
: initialValue).current;
// Get initial value from localStorage if available, otherwise use the resolved initial value
const getInitialStoredValue = useCallback(() => {
if (!isBrowser) {
return resolvedInitialValue;
}
try {
const item = window.localStorage.getItem(key);
const parsed = safeJsonParse(item);
// Use parsed value if valid, otherwise fallback to the resolved initial value
return parsed !== null && parsed !== void 0 ? parsed : resolvedInitialValue;
}
catch (error) {
console.error(`Error reading localStorage key “${key}”:`, error);
// Fallback to the resolved initial value on error
return resolvedInitialValue;
}
// Dependency is only on 'key' now, as initialValue is resolved outside and captured in resolvedInitialValue
}, [key, resolvedInitialValue]); // Depends on resolvedInitialValue
// State to store our value, initialized using the function above
const [storedValue, setStoredValue] = useState(getInitialStoredValue);
// Ref to store the current state setter function, avoids including it in effect deps
const setStateRef = useRef(setStoredValue);
useEffect(() => {
setStateRef.current = setStoredValue;
}, []);
// Effect to update localStorage when state changes
useEffect(() => {
if (!isBrowser)
return;
try {
const valueToStore = JSON.stringify(storedValue);
window.localStorage.setItem(key, valueToStore);
}
catch (error) {
console.error(`Error setting localStorage key “${key}”:`, error);
}
}, [key, storedValue]);
// Effect to listen for storage events from other tabs/windows
useEffect(() => {
if (!isBrowser)
return;
const handleStorageChange = (event) => {
if (event.key === key &&
event.newValue !== null &&
event.storageArea === window.localStorage) {
try {
const newValue = safeJsonParse(event.newValue);
if (newValue !== null) {
// Use the ref to get the current setter function
// Check if the new value is actually different to avoid unnecessary re-renders
// Note: This deep comparison might be expensive for large objects.
// Consider a shallow comparison or requiring serializable values if performance is critical.
if (JSON.stringify(newValue) !== JSON.stringify(storedValue)) {
setStateRef.current(newValue);
}
}
}
catch (error) {
console.error(`Error processing storage event for key “${key}”:`, error);
}
}
};
window.addEventListener('storage', handleStorageChange);
// Cleanup listener on unmount
return () => {
window.removeEventListener('storage', handleStorageChange);
};
// Ensure storedValue is a dependency here to re-evaluate the comparison inside the handler
// when the local state changes. key is also needed.
}, [key, storedValue]);
// Return the state and the setter function
return [storedValue, setStoredValue];
}
//# sourceMappingURL=useSyncedLocalStorage.js.map