news 2026/8/12 10:52:00

react - useSyncExternalStore 使用指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react - useSyncExternalStore 使用指南

文章目录

    • 一、 为什么需要 useSyncExternalStore?
      • 1. 解决并发渲染中的“撕裂(Tearing)”现象
      • 2. 传统 `useEffect` 模式的痛点
    • 二、 API 语法解析
    • 三、 实战案例
      • 场景 1:优雅地订阅浏览器 API(如网络状态 `navigator.onLine`)
      • 场景 2:实现一个简易全局 Store(手写简易版 Zustand)
    • 四、 关键踩坑点:死循环与引用一致性
      • ❌ 错误示范:引起无限死循环
      • ✅ 正确示范:保持不可变数据引用
    • 五、 总结与选型指南

useSyncExternalStore是 React 18 引入的一个专门用于订阅外部数据源(External Store)的 Hook。

虽然在日常业务组件开发中它的直接使用频率不如useStateuseEffect高,但它是各大状态管理库(如 Redux, Zustand)以及自定义 Browser API 订阅 Hook的底层核心基石。

一、 为什么需要 useSyncExternalStore?

1. 解决并发渲染中的“撕裂(Tearing)”现象

在 React 18 之前(同步渲染模式),渲染过程是一气呵成的,不会被中途打断。

而在 React 18 引入并发模式(Concurrent React)后,渲染任务变成了可中断、可恢复的。如果在渲染过程中,React 暂停了当前任务去处理更高优先级的用户交互,而此时外部数据源(非 React state 管理的数据,如全局 store 或 window 属性)发生了变更

[组件 A 渲染] ──> 读取外部值 = 1 │ ├─▶ (React 暂停渲染,让出主线程) │ 💥 外部数据源发生了更新:值变成了 2! │ [组件 B 渲染] ──> 读取外部值 = 2 最终 UI:同一张页面上,依赖相同数据的组件 A 和组件 B 展示了不一致的内容! 这就是所谓的“UI 撕裂 (Tearing)”。

2. 传统useEffect模式的痛点

在 React 18 之前,我们通常用useState+useEffect订阅外部数据:

// ❌ 旧做法:在并发模式下可能导致 UI 撕裂或闪烁 function useOnlineStatusOld() { const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { const handleStatus = () => setIsOnline(navigator.onLine); window.addEventListener('online', handleStatus); window.addEventListener('offline', handleStatus); return () => { window.removeEventListener('online', handleStatus); window.removeEventListener('offline', handleStatus); }; }, []); return isOnline; }
  • 缺陷 1(UI 撕裂):并发渲染期间无法保证状态读取的原子性。
  • 缺陷 2(额外重绘/闪烁)useEffect是在 DOM 挂载之后异步执行的,组件首次渲染先拿到旧值,挂载后再触发setState重新渲染,容易导致 UI 闪烁。

useSyncExternalStore的解决机制:它强制以**同步(Synchronous)**方式读取外部数据快照。如果在渲染过程中外部 store 发生了变动,React 会放弃当前的渲染,立刻重新使用最新的快照再次发起渲染,彻底消除了撕裂与闪烁。

二、 API 语法解析

const state = useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot? );
参数类型作用
subscribe(callback) => unsubscribe注册订阅函数。当外部数据发生变化时,调用传入的callback告诉 React:“数据变了,需要重新渲染!”
getSnapshot() => State获取当前外部数据的快照函数。必须返回不可变数据或同一引用(如果数据没变)
getServerSnapshot?(可选)() => State专为 SSR 服务端渲染提供的初始快照函数,避免服务端与客户端 Hydration 不匹配。

三、 实战案例

场景 1:优雅地订阅浏览器 API(如网络状态navigator.onLine

这是useSyncExternalStore在日常开发中最推荐的用例之一:

import { useSyncExternalStore } from 'react'; // 1. 定义订阅逻辑 function subscribeOnline(callback: () => void) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } // 2. 定义快照获取函数 function getOnlineSnapshot() { return navigator.onLine; } // 3. 封装为自定义 Hook export function useOnlineStatus() { return useSyncExternalStore( subscribeOnline, getOnlineSnapshot, () => true // SSR 场景下的默认兜底值 ); } // 4. 在组件中使用 function NetworkStatusBadge() { const isOnline = useOnlineStatus(); return ( <div className={isOnline ? 'bg-green' : 'bg-red'}> {isOnline ? '🌐 网络正常' : '⚠️ 已断网'} </div> ); }

场景 2:实现一个简易全局 Store(手写简易版 Zustand)

如果你想实现一个脱离 React 树管理(可以在 React 组件外直接store.setState(...))的全局状态,useSyncExternalStore是完美工具:

import { useSyncExternalStore } from 'react'; // 1. 创建外部 Store function createStore<T>(initialState: T) { let state = initialState; const listeners = new Set<() => void>(); return { getState: () => state, setState: (fnOrValue: T | ((prev: T) => T)) => { state = typeof fnOrValue === 'function' ? (fnOrValue as (prev: T) => T)(state) : fnOrValue; // 触发所有订阅者通知 React 更新 listeners.forEach(listener => listener()); }, subscribe: (listener: () => void) => { listeners.add(listener); return () => listeners.delete(listener); } }; } // 实例化 Store export const globalCountStore = createStore({ count: 0 }); // 2. 在组件中使用 function CounterApp() { // 订阅 Store const state = useSyncExternalStore( globalCountStore.subscribe, globalCountStore.getState ); return ( <div> <h3>Count: {state.count}</h3> <button onClick={() => globalCountStore.setState(s => ({ count: s.count + 1 }))}> 加 1 (可从任意地方触发) </button> </div> ); }

四、 关键踩坑点:死循环与引用一致性

在写getSnapshot时,绝对不能每次都返回一个新创建的对象

❌ 错误示范:引起无限死循环

function BadComponent() { const state = useSyncExternalStore( store.subscribe, // ❌ 错误!每次调用 getSnapshot 都返回一个新的对象引用 // React 会认为数据时刻在变化,从而引发无限 Re-render 崩溃! () => ({ count: store.getState().count }) ); }

✅ 正确示范:保持不可变数据引用

// ✅ 方式 A:只返回基本数据类型(Primitive Value) const count = useSyncExternalStore( store.subscribe, () => store.getState().count // 返回 number 基础类型,天然支持 Object.is 比较 ); // ✅ 方式 B:如果必须返回对象,请确保 Store 内部更新时采用不可变(Immutable)深/浅拷贝 const state = useSyncExternalStore( store.subscribe, store.getState // getState 保证在未更新时返回完全相同的对象引用 );

五、 总结与选型指南

维度useState/useReduceruseSyncExternalStore
状态归属React 组件树内部(React 状态)React 外部(如全局 Store, Window API, LocalStorage)
渲染模式异步并发/批处理渲染强制同步读取快照,规避撕裂
适用场景组件内部 UI 状态(如弹窗开关、表单输入)状态库开发、浏览器事件订阅、跨框架全局状态共享

💡核心建议:普通业务开发中,处理组件自身的 State 依然优先选择useState;只有在需要**订阅 React 框架外部的数据源(如 window resize、media queries、自定义全局事件通知、封装 Redux/Zustand 这类外部 Store)**时,才使用useSyncExternalStore


🚀 感谢阅读!想了解更多?

📖 我的博客网站 | 记录思考,分享干货
🏡 我的个人主页 | 关于我、开源项目


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/12 10:51:01

CentOS 7 U盘安装实战:彻底解决dracut-initqueue timeout报错

1. 项目概述&#xff1a;从U盘启动盘到系统安装的完整闭环 如果你是一名运维工程师、开发者&#xff0c;或者只是想在自己的旧电脑上搭建一个稳定服务器的技术爱好者&#xff0c;那么通过U盘安装CentOS 7几乎是一个必经的“仪式”。这个过程听起来简单——下载镜像、刻录U盘、引…

作者头像 李华
网站建设 2026/8/12 10:50:51

bcftools实战指南:VCF文件处理核心技巧与性能优化

1. 项目概述&#xff1a;为什么你需要掌握bcftools如果你正在处理高通量测序数据&#xff0c;尤其是涉及变异检测&#xff08;Variant Calling&#xff09;的工作流&#xff0c;那么你迟早会与VCF&#xff08;Variant Call Format&#xff09;文件打交道。VCF文件是存储基因变异…

作者头像 李华
网站建设 2026/8/12 10:46:35

力维网络跨行业控价实践:以电商价格监测破解价格治理难题

力维网络深耕品牌渠道管控领域&#xff0c;聚焦电商价格监测、品牌控价、低价治理核心服务&#xff0c;面向家电、母婴、药品、3C 数码、食品、服装等众多行业品牌提供全链路渠道管控解决方案。经过多年项目沉淀&#xff0c;力维网络已服务大量国内外品牌客户&#xff0c;深度理…

作者头像 李华
网站建设 2026/8/12 10:45:32

非小米电脑安装小米妙享中心:绕过机型认证的完整指南

1. 项目缘起&#xff1a;一个被“机型认证”卡住的刚需如果你和我一样&#xff0c;手里有一台非小米品牌的电脑&#xff0c;或者是一台比较老款的小米笔记本&#xff0c;却对小米生态里那个能在电脑、手机、平板之间无缝流转文件、接力应用、共享屏幕的“小米妙享中心”垂涎已久…

作者头像 李华