同步外部数据源
同步外部数据源 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
主题偏好、登录信息、播放器进度这类「React 之外的数据」需要同步进组件。这一课你会用 useSyncExternalStore 写出订阅三件套,理解快照稳定性这条硬规矩,并验证多个组件读取时永远一致。
问题场景
学习进度存在模块级变量 store.progress,两个组件各自用 useState 复制了一份,再想办法同步:
// 组件 A
const [progress, setProgress] = useState(store.progress);
useEffect(() => {
const id = setInterval(() => setProgress(store.progress), 1000); // 轮询兜底
return () => clearInterval(id);
}, []);
// 组件 B:又复制了一份,忘了轮询
结果:A 显示「3/10」,B 还停在「2/10」。同一个数据,两份真相,必然漂移。
核心解释
这类数据的共同点:owner 在 React 外面——模块变量、localStorage、浏览器连接状态、第三方播放器。它们可变,也不提供 React 认识的更新通知;用 state 复制加 Effect 轮询,等于到处造假副本。
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) 是官方为此准备的接口:
getSnapshot:读当前值的纯函数。核心纪律:同一状态必须返回同一个引用。要返回缓存的对象,而不是每次现场构造{ done: 3 }——新引用会被 React 判定「快照变了」,触发渲染、再读快照、又是新引用,无限循环直到页面卡死。subscribe(callback):登记监听并返回取消函数。状态变化时调用callback,React 收到通知后重新读快照、按需渲染。getServerSnapshot:服务端渲染时的取值。纯浏览器应用可以省略;SSR 项目必传,否则水合时报错。
多个组件订阅同一个 store 时,React 内部只维护一份快照,谁读到都一样——一致性由「单一数据源」保证,不靠运气。
示例拆解
// 模块级 store:单一数据源(真实项目通常放单独文件)
let progress = { done: 3, total: 10 };
const listeners = new Set();
const courseStore = {
subscribe(callback) {
listeners.add(callback);
return () => listeners.delete(callback); // 取消订阅
},
getSnapshot() {
return progress; // 返回缓存的同一个对象
},
finishNext() {
progress = { ...progress, done: Math.min(progress.done + 1, progress.total) };
listeners.forEach((l) => l()); // 通知所有订阅者
},
};
// 组件里一行接入
function ProgressPanel() {
const p = useSyncExternalStore(courseStore.subscribe, courseStore.getSnapshot);
return <p>进度:{p.done}/{p.total}</p>;
}
finishNext用展开运算生成新对象再整体替换——快照不可变,React 才能靠引用比较发现变化。- 直接
progress.done++而不换引用,React 读快照发现引用没变,会认为「没更新」。 - 订阅通知只应在外部事件里发起(点击、推送、storage 事件),不要在渲染期间调用。
动手任务
打开本课实验区:
- 点「完成下一课(通知)」,验证进度条和文字面板两个组件同时更新且数字一致。
- 点「改 store 但不通知」,观察界面不动——快照变了但没有订阅通知,React 不知道;再点一次会通知的按钮,两块面板一起跳到最新值。
- 给 store 加
reset():恢复初始进度并通知,验证两个面板回到同一状态。
常见错误
- getSnapshot 返回新对象或新数组:无限渲染、页面卡死;快照必须缓存,变化时整体替换。
- 改可变字段而不换引用:
snapshot.done++不触发任何更新;用展开运算生成新对象。 - 订阅函数每次渲染都是新的:传模块层的
store.subscribe没问题,但传内联箭头函数(cb) => store.sub(cb)会导致每次渲染都退订重订;把订阅函数也定义在模块层。 - SSR 项目漏掉 getServerSnapshot:服务端渲染直接报错;纯客户端应用不受影响。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能说出三件套各自的职责,以及 subscribe 的返回值要求
- 知道 getSnapshot 的引用稳定性是硬要求,能解释违反的后果
- 实验里「多面板一致」和「不通知就不更新」两点都验证过
参考资料
- React 官方文档:useSyncExternalStore —— API 语义与快照稳定性要求
- React 官方文档:你可能不需要 Effect——订阅外部 store —— 什么时候该用它而不是 useEffect
读完这一课了?
标记完成后,工作台会安排接下来的学习。