目录
- 主要用途
- [一个简洁示例:监听 window size](#一个简洁示例:监听 window size)
- [为什么 React 提供了这个](#为什么 React 提供了这个)
- [useSyncExternalStore 的正确性保证来自](#useSyncExternalStore 的正确性保证来自)
- 总结
- [更实际的例子:模拟 Redux Store](#更实际的例子:模拟 Redux Store)
- 总结对比
useSyncExternalStore
是 React 18 引入的一个底层 hook,用于安全地订阅外部状态源 (external store),确保它在 concurrent rendering(并发渲染)中表现一致,避免数据"撕裂"(tearing)。
主要用途
用于和 非 React 管理的状态系统(比如 Redux、Zustand、原生订阅、window 状态等)对接。
typescript
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe(callback)
:注册一个监听器,状态变化时调用。getSnapshot()
:返回当前的状态快照(同步读取状态)。getServerSnapshot()
:可选,用于 SSR。
一个简洁示例:监听 window size
typescript
import { useSyncExternalStore } from 'react';
function subscribe(callback: () => void) {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}
function getSnapshot() {
return window.innerWidth;
}
export function WindowWidth() {
const width = useSyncExternalStore(subscribe, getSnapshot);
return <div>Window width: {width}px</div>;
}
⚠️
useEffect + useState
也可以实现这个功能,但在并发模式下可能出现旧值,这时候useSyncExternalStore
就保证了数据和 UI 是"同步"的。
为什么 React 提供了这个
React 18 并发模式带来了潜在的"撕裂"问题(UI 和 state 不一致),而 useSyncExternalStore
解决了这个问题:
typescript
// ❌ 以前这样会有问题:
const [count, setCount] = useState(store.get());
useEffect(() => store.subscribe(setCount), []);
在并发渲染中,store.get()
可能获取的是旧的 state,造成 UI 和实际 state 不同步。
让我们来深入看看这个问题👇
问题:useEffect + useState
在并发模式下可能造成 "旧值"(Stale Value)或 UI 撕裂(Tearing)
常见的旧写法(会有问题)
typescript
const [value, setValue] = useState(getSnapshot());
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setValue(getSnapshot()); // 订阅后更新 state
});
return unsubscribe;
}, []);
为什么上面这个写法有风险?
在 React 18 的并发模式(Concurrent Mode)下:
- 组件的 渲染是可中断的 ,React 可能会 暂停当前渲染任务,然后再重新继续(或丢弃)。
- 在这个过程中,外部的 store 状态可能已经改变了。
- 但
useState(getSnapshot())
的值已经被"固定",就可能是旧的。 - 最终造成"UI 显示的是旧状态",但底层状态已经变了 ------ 撕裂(tearing)现象。
解决方式:使用 useSyncExternalStore
👇
typescript
const value = useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot // 可选(SSR 用)
);
这个 hook:
- React 会在渲染阶段 读取
getSnapshot()
的值。 - 它保证:如果状态发生变化,React 会重新触发渲染,并拿到最新的快照。
- 而且它是为并发模式设计的,自动避免 stale value 和 tearing 问题。
举个更直观的例子👇
- 你在渲染一个表单,值来自外部 store。
- 用户修改 store 的值,同时 React 还没完成这个组件的渲染。
useState(getSnapshot())
的值已经是旧的,但 UI 还在用它渲染。- 就会出现:表单渲染出来是旧值,但 store 里是新值 → 撕裂了!
useSyncExternalStore 的正确性保证来自
- 它在 render 阶段同步读取快照;
- 并且在必要时自动强制重渲染。
总结
方案 | 是否支持并发模式 | 会不会 stale / tearing |
---|---|---|
useState + useEffect |
❌ 不可靠 | ⚠️ 有风险 |
useSyncExternalStore |
✅ 支持 | ✅ 不会撕裂 |
如果你在写一个"订阅型"的 hook,比如监听媒体查询 、WebSocket 、Redux 、Zustand 、window 变化 等------就推荐用 useSyncExternalStore
。
更实际的例子:模拟 Redux Store
typescript
// store.ts
let state = { count: 0 };
let listeners: (() => void)[] = [];
export function getSnapshot() {
return state;
}
export function subscribe(listener: () => void) {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
}
export function increment() {
state = { count: state.count + 1 };
listeners.forEach(l => l());
}
typescript
// Counter.tsx
import { useSyncExternalStore } from 'react';
import { getSnapshot, subscribe, increment } from './store';
function Counter() {
const state = useSyncExternalStore(subscribe, getSnapshot);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
总结对比
用途 | 用法 |
---|---|
本地 React 状态管理 | useState / useReducer |
全局状态库(Redux 等) | 推荐使用 useSyncExternalStore |
原生订阅类对象(如 window、WebSocket) | useSyncExternalStore |
兼容 SSR | 支持 getServerSnapshot |
如果你用的是 Redux 4.2+,它的 useSelector
就内部用的 useSyncExternalStore
。
如果你想自己实现一个小型全局状态管理(类 Zustand),也可以直接用这个。