shijing-v5-dev/src/hooks/useGlow.tsx(39 行)+ hooks/useHydrated.ts(12 行)+ hooks/useParticles.ts(31 行)+ lib/tree-store.ts(62 行)useSyncExternalStore 统一管理"浏览器存储型共享状态",一次解决多组件同步、SSR 水合不一致、避免 effect 里 setState 三个问题。
既要在多个组件间同步,又要在刷新后从 localStorage 恢复;
如果每个组件各自 useEffect + setState 读取,会写出重复、竞态、
以及"服务端渲染和客户端不一致(水合报错)"的经典问题。
共享状态 + 订阅(useGlow.tsx 全文件,这是模式的完整体现):
const GLOW_EVENT = "shijing:glow-change";
export function getGlow(): boolean {
return safeGetRaw(STORAGE.glow, "on") === "on"; // 从 localStorage 读
}
export function toggleGlow(): void {
const next = !getGlow();
safeSetRaw(STORAGE.glow, next ? "on" : "off");
window.dispatchEvent(new CustomEvent(GLOW_EVENT)); // 广播变更
}
function subscribe(cb: () => void): () => void {
window.addEventListener(GLOW_EVENT, cb); // 同页面内变更
window.addEventListener("storage", cb); // 其他标签页变更
return () => { window.removeEventListener(GLOW_EVENT, cb); window.removeEventListener("storage", cb); };
}
const getServerSnapshot = () => true; // 服务端渲染时的默认值
export function GlowProvider({ children }: { children: ReactNode }) {
const glow = useSyncExternalStore(subscribe, getGlow, getServerSnapshot);
return <GlowCtx.Provider value={{ glow, toggleGlow }}>{children}</GlowCtx.Provider>;
}
export function useGlow() { return useContext(GlowCtx); }
水合状态 Hook(useHydrated.ts):
export function useHydrated() {
// 服务端返回 false,客户端返回 true,借此判断"是否已水合完成"
return useSyncExternalStore(subscribe, () => true, () => false);
}
对话树共享 store(tree-store.ts 描述):共享快照 + 500ms 防抖持久化 +shijing:tree-changed / storage 订阅,多个组件共用一份树数据。
storage 事件(跨标签页变更)getServerSnapshot 给默认值:服务端渲染有确定值,从根上避免水合不匹配useHydrated 通用模式:任何"必须等客户端才能显示"的地方都能复用useSyncExternalStore 是官方 API)getGlow/toggleGlow 换成你的存取函数即可)@testing-library/react 测订阅行为use-local-store(npm):传入 storageKey + defaultValue,返回 [value, setValue],自动处理订阅、跨标签页同步、SSR 默认值
useState + useEffect 读 localStorage"(会闪一下、会水合不一致、多组件不同步)
use-local-storage 这类库做的事类似;你这份胜在手写、透明、和项目状态管理统一useSyncExternalStore 更轻第 11 课(组件化:React/Next.js)、第 19 课(交付验收)