← 精品代码功能 · 可复用实现库

精品功能 15:前端共享状态 —— 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 订阅,多个组件共用一份树数据。

设计亮点

  1. 一个 Hook 解决三类问题:跨组件同步 + 持久化恢复 + SSR 一致性
  2. 外部存储作为"单一数据源":不复制一份到组件 state,避免不同步
  3. 监听两种事件:自定义事件(同页变更)+ storage 事件(跨标签页变更)
  4. getServerSnapshot 给默认值:服务端渲染有确定值,从根上避免水合不匹配
  5. useHydrated 通用模式:任何"必须等客户端才能显示"的地方都能复用
  6. 收敛重复逻辑:项目里有好几处 localStorage 读写,统一到 store + hook 后删掉重复代码

可复用性评估

开源化建议

返回 [value, setValue],自动处理订阅、跨标签页同步、SSR 默认值

(会闪一下、会水合不一致、多组件不同步)

对照开源

相关课程

第 11 课(组件化:React/Next.js)、第 19 课(交付验收)