WHAT - React 安全地订阅外部状态源 - useSyncExternalStore

目录

  • 主要用途
  • [一个简洁示例:监听 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 问题

举个更直观的例子👇

  1. 你在渲染一个表单,值来自外部 store。
  2. 用户修改 store 的值,同时 React 还没完成这个组件的渲染。
  3. useState(getSnapshot()) 的值已经是旧的,但 UI 还在用它渲染。
  4. 就会出现:表单渲染出来是旧值,但 store 里是新值 → 撕裂了!

useSyncExternalStore 的正确性保证来自

  • 它在 render 阶段同步读取快照
  • 并且在必要时自动强制重渲染。

总结

方案 是否支持并发模式 会不会 stale / tearing
useState + useEffect ❌ 不可靠 ⚠️ 有风险
useSyncExternalStore ✅ 支持 ✅ 不会撕裂

如果你在写一个"订阅型"的 hook,比如监听媒体查询WebSocketReduxZustandwindow 变化 等------就推荐用 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),也可以直接用这个。

相关推荐
风中芦苇啊6 小时前
从直接生成到受控配置:新一代图表Agent的SQL安全生成范式
数据库·sql·安全
泛普软件6 小时前
企业项目管理软件如何选型?统筹多项目资源把控交付与盈利水平
大数据·安全
anOnion7 小时前
Agentic 前端开发之 实时显示 AI Agent 终端输出
前端·javascript·人工智能
这是个栗子7 小时前
【前端性能优化】优化数据加载:用 Promise.all 从串行到并行
前端·javascript·性能优化·异步编程·前端优化·promise.all
fei_sun8 小时前
黑洞路由(Null Route/空接口路由)
服务器·前端·javascript
云水一下8 小时前
DVWA从入门到精通(四):CSRF(跨站请求伪造)
安全·csrf·dvwa
tuddy7894648 小时前
Codex++ 安全边界探秘:从模型能力到风险防御
人工智能·python·安全
hbugs0019 小时前
【案例分享】全网首个华三数据中心流量可视化实验,基于EVE-NG V7平台
网络·网络协议·安全·devops·eve-ng
深盾科技_Virbox9 小时前
深盾科技·Virbox产品体系全景解读:软件安全如何从加密锁走向全生命周期
java·大数据·算法·安全·软件需求
摇滚侠9 小时前
方法 A 等方法 B 执行完再执行 叫同步调用还是异步调用 JS 默认是同步调用还是异步调用
开发语言·javascript·ecmascript