fix: cache useSyncExternalStore snapshot to prevent infinite re-render

getSnapshot() was calling JSON.parse() on every invocation, returning
a new array reference each time. useSyncExternalStore uses Object.is
comparison, so it saw a "new" value on every render and triggered
an infinite update loop. Fix by caching the parsed result and only
re-parsing when the raw localStorage value changes.
This commit is contained in:
SnapOtter
2026-06-14 20:31:49 +08:00
parent f387e98fff
commit f68fcb8ca6
+16 -4
View File
@@ -2,17 +2,28 @@ import { useSyncExternalStore } from "react";
const STORAGE_KEY = "snapotter-recent-tools"; const STORAGE_KEY = "snapotter-recent-tools";
const MAX_RECENT = 5; const MAX_RECENT = 5;
const EMPTY: string[] = [];
let listeners: Array<() => void> = []; let listeners: Array<() => void> = [];
let cachedRaw: string | null = null;
let cachedParsed: string[] = EMPTY;
function getSnapshot(): string[] { function getSnapshot(): string[] {
try {
const raw = localStorage.getItem(STORAGE_KEY); const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : []; if (raw !== cachedRaw) {
cachedRaw = raw;
try {
cachedParsed = raw ? JSON.parse(raw) : EMPTY;
} catch { } catch {
return []; cachedParsed = EMPTY;
} }
} }
return cachedParsed;
}
function getServerSnapshot(): string[] {
return EMPTY;
}
function subscribe(listener: () => void) { function subscribe(listener: () => void) {
listeners.push(listener); listeners.push(listener);
@@ -25,9 +36,10 @@ export function recordRecentTool(toolId: string) {
const current = getSnapshot(); const current = getSnapshot();
const updated = [toolId, ...current.filter((id) => id !== toolId)].slice(0, MAX_RECENT); const updated = [toolId, ...current.filter((id) => id !== toolId)].slice(0, MAX_RECENT);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
cachedRaw = null;
listeners.forEach((l) => l()); listeners.forEach((l) => l());
} }
export function useRecentTools(): string[] { export function useRecentTools(): string[] {
return useSyncExternalStore(subscribe, getSnapshot, () => []); return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
} }