useSyncExternalStore 解决了什么问题?
简化版
useSyncExternalStore 用来安全订阅 React 外部的数据源,例如 Redux store、浏览器 API、手写状态容器。它让 React 在并发渲染下能一致地读取快照,避免 tearing,也提供 SSR 快照能力。
详细版
外部 store 不由 React state 管理,但 React 组件需要订阅它。
const value = useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot
)
subscribe 负责订阅变化,getSnapshot 返回当前快照,getServerSnapshot 用于 SSR。它主要给状态库作者和复杂外部订阅场景使用。普通组件内部状态仍然用 useState、useReducer,不要为了显得高级滥用它。
完整版教学
一、外部状态不在 React 的管理范围内
React 自己的 state 更新能被调度和追踪。 但 Redux store、浏览器在线状态、localStorage 事件、WebSocket 缓存等数据源,可能在 React 外部变化。 组件想读取这些数据,就需要建立订阅。
function useOnlineStatus() {
return useSyncExternalStore(
subscribeOnline,
() => navigator.onLine,
() => true
)
}
如果直接在 render 中读取外部变量,React 不知道它什么时候变。 如果只用 effect 手动 setState,在并发渲染下又可能出现一致性问题。
二、它的三个参数分工很明确
subscribe 告诉 React 数据源变了怎么通知。
getSnapshot 告诉 React 当前值是什么。
getServerSnapshot 告诉 React 服务端渲染时快照是什么。
function subscribe(callback: () => void) {
store.on('change', callback)
return () => store.off('change', callback)
}
function getSnapshot() {
return store.getState()
}
| 参数 | 职责 | 注意点 |
|---|---|---|
subscribe | 订阅外部变化 | 必须返回取消订阅函数 |
getSnapshot | 读取客户端快照 | 相同数据应返回相同引用 |
getServerSnapshot | 读取服务端快照 | SSR 时保持一致 |
如果 getSnapshot 每次都返回新对象,即使数据没变,也可能导致无限重渲染。
快照的稳定性是这题的重要细节。
三、它防的是 tearing 这类一致性问题
tearing 可以理解成同一次渲染里,不同组件看到外部 store 的不同版本。
并发渲染允许 React 暂停和恢复,如果外部数据在中间变化,就可能出现不一致。
useSyncExternalStore 给 React 一个标准协议,让它能在提交前检查快照一致性。
组件 A 读到 store v1
渲染暂停
store 变成 v2
组件 B 读到 store v2
同一屏出现 v1 和 v2 -> tearing
这个问题在普通小页面不常被肉眼看到。 但对状态库、协作编辑、实时数据面板来说非常关键。
四、外部 store 的快照要可缓存
getSnapshot 返回值用于比较是否需要重新渲染。
如果每次都返回 { ...state } 新对象,React 会认为快照一直变。
正确做法是 store 内部维护不可变状态,没变化时返回同一个引用。
// 不推荐
function getSnapshot() {
return { ...store.state }
}
// 推荐
function getSnapshot() {
return store.state
}
假设组件每秒渲染 10 次,getSnapshot 每次都返回新对象,就可能制造 10 次无意义更新。
大型应用里这种细节会变成性能坑。
五、SSR 需要 getServerSnapshot 保持初始一致
服务端渲染时没有浏览器环境,也可能没有客户端 store 的最新状态。
getServerSnapshot 用来提供服务端快照。
客户端 hydration 时,它应与初始客户端快照匹配,否则会出现不一致。
const theme = useSyncExternalStore(
subscribeTheme,
getClientTheme,
() => 'light'
)
如果服务端返回 light,客户端首次读取 dark,页面可能 hydration 不匹配。 这时要通过内联脚本、cookie 或服务端请求头让初始主题一致。
六、普通业务组件不一定需要直接用它
多数开发者通过 Redux、Zustand 等库间接使用它。 这些库会封装外部 store 订阅细节。 只有你自己写状态库、订阅浏览器外部源、做实时数据容器时,才需要直接掌握。
记忆钩子:useSyncExternalStore 是 React 和外部 store 之间的同步协议。
面试回答不要把它说成普通状态 Hook。 它的关键词是外部数据源、订阅、快照、一致性和 SSR。
七、常见误区与追问
- 误区:useSyncExternalStore 可以替代 useState。 它服务外部 store 订阅,组件内部状态仍用
useState或useReducer。 - 误区:getSnapshot 每次返回新对象没问题。 快照引用不稳定会导致不必要渲染,甚至循环更新。
- 误区:subscribe 不需要清理。 必须返回取消订阅函数,否则会造成泄漏和重复通知。
- 追问:tearing 是什么? 同一轮 UI 中不同组件看到外部 store 的不同版本,造成界面不一致。
- 追问:状态库为什么会用它? 状态库的数据在 React 外部,需要用标准订阅协议适配并发渲染。
- 追问:SSR 为什么需要第三个参数? 服务端没有客户端环境,需要提供可匹配的初始快照。
八、加强记忆
这题按“外部 store 三件套”记:订阅变化、读取快照、服务端快照。它不是普通 state API,而是保证 React 与外部状态在并发和 SSR 下安全对接的协议。重点补上 tearing 和快照引用稳定性。