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),也可以直接用这个。

相关推荐
Hello.Reader4 小时前
Flink ZooKeeper HA 实战原理、必配项、Kerberos、安全与稳定性调优
安全·zookeeper·flink
智驱力人工智能4 小时前
小区高空抛物AI实时预警方案 筑牢社区头顶安全的实践 高空抛物检测 高空抛物监控安装教程 高空抛物误报率优化方案 高空抛物监控案例分享
人工智能·深度学习·opencv·算法·安全·yolo·边缘计算
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
数据与后端架构提升之路5 小时前
论系统安全架构设计及其应用(基于AI大模型项目)
人工智能·安全·系统安全
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
市场部需要一个软件开发岗位6 小时前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
lingggggaaaa6 小时前
安全工具篇&动态绕过&DumpLsass凭据&Certutil下载&变异替换&打乱源头特征
学习·安全·web安全·免杀对抗
凯子坚持 c6 小时前
CANN-LLM:基于昇腾 CANN 的高性能、全功能 LLM 推理引擎
人工智能·安全
铅笔侠_小龙虾7 小时前
Flutter 实战: 计算器
开发语言·javascript·flutter
大模型玩家七七7 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习