dashboard design changes
This commit is contained in:
86
src/lib/usePersistentFlag.ts
Normal file
86
src/lib/usePersistentFlag.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
'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];
|
||||
}
|
||||
Reference in New Issue
Block a user