Files
loyaly-merchant/src/lib/usePersistentFlag.ts
2026-08-05 18:34:37 +05:30

87 lines
2.5 KiB
TypeScript

'use client';
import {useCallback, useSyncExternalStore} from 'react';
/**
* A boolean persisted in localStorage, read without a hydration mismatch.
*
* useSyncExternalStore already solves the hydration problem on its own:
* React uses `getServerSnapshot` for the SSR render AND for the first client
* render, so the two markups match, then immediately re-reads `getSnapshot`
* and re-renders with the stored value. No `isHydrated` flag is needed — and
* adding one means a setState inside an effect, which React's lint rule
* rejects and which costs an extra render pass to do what the hook already
* does natively.
*
* Writes notify every subscriber, so two components reading the same key never
* disagree, and the `storage` event keeps other tabs in step.
*/
const listeners = new Set<() => void>();
function emit() {
listeners.forEach((l) => l());
}
function subscribe(onChange: () => void) {
listeners.add(onChange);
window.addEventListener('storage', onChange);
return () => {
listeners.delete(onChange);
window.removeEventListener('storage', onChange);
};
}
/**
* The same store, but "never set" stays distinguishable from "set to false".
*
* The sidebar needs that distinction: its default is per-device (a tablet
* starts collapsed, a desktop starts expanded), so a stored `false` has to
* mean "this merchant expanded it" and not "no preference yet". Collapsing
* the two would either strand tablets expanded or force desktops to re-expand
* on every first visit.
*/
export function usePersistentTriState(
key: string,
): [boolean | null, (next: boolean) => void] {
const getSnapshot = useCallback(() => {
try {
const raw = window.localStorage.getItem(key);
return raw === null ? null : raw === 'true';
} catch {
// Private mode / storage disabled — degrade to "no preference" rather
// than taking the whole shell down over a preference.
return null;
}
}, [key]);
const getServerSnapshot = useCallback(() => null, []);
const value = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot,
);
const set = useCallback(
(next: boolean) => {
try {
window.localStorage.setItem(key, String(next));
} catch {
/* ignore — the notify below still updates this session */
}
emit();
},
[key],
);
return [value, set];
}
export function usePersistentFlag(
key: string,
fallback = false,
): [boolean, (next: boolean) => void] {
const [stored, set] = usePersistentTriState(key);
return [stored ?? fallback, set];
}