【React】Hooks 解锁外部状态安全订阅 useSyncExternalStore 应用与最佳实践

一、背景

  1. useSyncExternalStore 是 React 18 引入的一个 Hook;
  2. 用于从外部存储(例如状态管理库、浏览器 API 等)获取状态并在组件中同步显示。这对于需要跟踪外部状态的应用非常有用。

二、场景

  1. 订阅外部 store 例如(redux,mobx,Zustand,jotai) vue的 vuex pinia
  2. 订阅浏览器API 例如(online,storage,location, history hash)等
  3. 抽离逻辑,编写自定义hooks
  4. 服务端渲染支持

三、用法

const state = useSyncExternalStore(subscribe: (onStoreChangeCallback: () => void) => () => void,getSnapshot: () => Snapshot,getServerSnapshot?: () => Snapshot
);
  • subscribe:订阅函数,接收一个回调函数 onStoreChange,当外部状态变化时调用该回调。需返回一个清理函数,用于取消订阅。
  • getSnapshot:获取当前数据源的快照(当前状态)。
  • getServerSnapshot(可选):服务端渲染时使用的快照函数,确保客户端与服务端状态一致。

返回值:该 res 的当前快照,可以在你的渲染逻辑中使用

const subscribe = (callback: () => void) => {// 订阅callback() return () => { // 取消订阅}
}const getSnapshot = () => {return data
}const res = useSyncExternalStore(subscribe, getSnapshot)

四、订阅浏览器 Api 实现自定义hook(useStorage)

  1. 我们实现一个useStorage Hook,用于订阅 localStorage 数据。这样做的好处是,我们可以确保组件在 localStorage 数据发生变化时,自动更新同步。
  2. 我们将创建一个 useStorage Hook,能够存储数据到 localStorage,并在不同浏览器标签页之间同步这些状态
  3. 此 Hook 接收一个键值参数用于存储数据的键名,还可以接收一个默认值用于在无数据时的初始化。

在 hooks/useStorage.ts 中定义 useStorage Hook:

import { useSyncExternalStore } from "react";/*** 自定义 Hook,用于在 React 组件中同步 localStorage 状态* @param key - localStorage 存储的键名* @param initValue - 初始值,当 localStorage 中不存在对应键值时使用* @returns [存储的值, 更新函数] - 返回一个数组,包含当前值和更新函数*/
export const useStroage = (key: string, initValue: any) => {/*** 订阅者: 订阅 storage 变化* @param callback - storage 变化时的回调函数* @returns 取消订阅的函数*/const subscribe = (callback: () => void) => {// 订阅浏览器的 storage 事件window.addEventListener('storage', callback);return () => {// 取消订阅window.removeEventListener('storage', callback);};};/*** 获取当前 localStorage 中存储的值* @returns 当前存储的值或初始值*/const getSnapshot = () => {const storedValue = localStorage.getItem(key);return storedValue ? JSON.parse(storedValue) : initValue;};// 使用 React 的 useSyncExternalStore 同步外部状态const value = useSyncExternalStore(subscribe, getSnapshot);/*** 更新 localStorage 中的值* @param value - 要存储的新值*/const updateStorage = (value: any) => {// 将值存储到 localStoragelocalStorage.setItem(key, JSON.stringify(value));// 触发 storage 事件,通知其他订阅者window.dispatchEvent(new StorageEvent('storage', { key }));};return [value, updateStorage];
};

测试使用 自定义 hooks

import { useStroage } from './hooks/useStrage'function App() {// 使用 useStroage hook 管理计数状态,初始值为1// count: 当前计数值// setCount: 更新计数的函数const [count, setCount] = useStroage('count', 1);return (<>{/* 显示当前计数值 */}<div>{count}</div>{/* 增加按钮 - 点击时计数值加1 */}<button onClick={() => setCount(count + 1)}>Add</button>{/* 减少按钮 - 点击时计数值减1 */}<button onClick={() => setCount(count - 1)}>Sub</button></>);
};export default App;

五、获取浏览器url信息 + 参数

实现一个简易的useHistory Hook,获取浏览器url信息 + 参数

让我们在组件中使用这个 useHistory Hook,实现基本的前进、后退操作以及程序化导航。

效果演示

  1. history:这是 useHistory 返回的当前路径值。每次 URL 变化时,useSyncExternalStore 会自动触发更新,使 history 始终保持最新路径。
  2. push 和 replace:
    1. 点击“跳转”按钮调用 push(“/push”),会将 /push推入历史记录;
    2. 点击“替换”按钮调用 replace(“/replace”),则会将当前路径替换为 /replace。
import { useSyncExternalStore } from "react";/*** 自定义 Hook,用于在 React 组件中同步和管理浏览器历史记录状态* @returns [当前URL, push方法, replace方法] - 返回一个元组,包含当前URL和两个导航方法*/
export const useHistory = () => {/*** 订阅浏览器历史记录变化* @param callback - 历史记录变化时的回调函数* @returns 取消订阅的函数*/const subscribe = (callback: () => void) => {// 监听 popstate 事件 - 用于捕获浏览器前进/后退按钮的操作, // history 底层监听的是 popstate 事件window.addEventListener('popstate', callback);// 监听 hashchange 事件 - 用于捕获 URL hash 部分的变化 // hash 底层监听的是 hashchange 事件  window.addEventListener('hashchange', callback);// 返回清理函数return () => {window.removeEventListener('popstate', callback);window.removeEventListener('hashchange', callback);};};/*** 获取当前浏览器 URL* @returns 当前完整的 URL 字符串* 如果 getSnapshot 返回值和上一次不同时,React 会重新渲染组件。* 如果总是返回一个不同的值,会进入到一个无限循环,并产生这个报错。* - 比如数组对象这中引用类型。getSnapshot 返回值和上一次不同时,React 会重新渲染组件。* - 解决方式需要我们手动去比对更新。*/const getSnapshot = () => {return window.location.href;};// 使用 React 的 useSyncExternalStore 同步 URL 状态const url = useSyncExternalStore(subscribe, getSnapshot);/*** 向历史记录栈中推入新的记录* @param url - 目标 URL*/const push = (url: string) => {window.history.pushState(null, '', url);// 手动触发 popstate 事件,因为 pushState 不会自动触发window.dispatchEvent(new PopStateEvent('popstate'));};/*** 替换当前的历史记录* @param url - 目标 URL*/const replace = (url: string) => {window.history.replaceState(null, '', url);// 手动触发 popstate 事件,因为 replaceState 不会自动触发window.dispatchEvent(new PopStateEvent('popstate'));};return [url, push, replace] as const;
};

使用

import { useHistory } from './hooks/useHistory'/*** App 组件 - 演示 useHistory 自定义 Hook 的使用* @returns React 组件*/
function App() {// 使用 useHistory hook 获取当前 URL 和导航方法const [url, push, replace] = useHistory();return (<>{/* 显示当前 URL */}<div>{url}</div>{/* 使用 push 方法导航到 /push 路径 */}<button onClick={() => push('/push')}>/push</button>{/* 使用 replace 方法替换当前路径为 /replace */}  <button onClick={() => replace('/replace')}>/replace</button></>);
};export default App;

六、注意事项

  1. 避免条件渲染:不应基于 useSyncExternalStore 返回的状态值进行条件渲染(如动态加载懒加载组件),因为外部状态变化无法被标记为非阻塞更新,可能触发 Suspense 后备方案,导致用户体验不佳。

  2. 不可变快照getSnapshot 返回的快照必须是不可变的。若底层状态变化,需返回新的不可变快照。

  3. 清理订阅subscribe 函数需返回清理函数,确保组件卸载时取消订阅,防止内存泄漏。

  4. 如果 getSnapshot 返回值和上一次不同时,React 会重新渲染组件。如果总是返回一个不同的值,会进入到一个无限循环,并产生这个报错。

    Uncaught (in promise) Error: Maximum update depth exceeded. 
    This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
    React limits the number of nested updates to prevent infinite loops.
    

七、对比 useEffect + useState

  1. 并发渲染安全性useEffect + useState 在并发模式下可能导致旧值问题,而 useSyncExternalStore 通过同步读取快照确保状态一致性。
  2. 适用场景useSyncExternalStore 更适合需要安全订阅外部状态源的场景,而 useEffect + useState 适用于简单的状态管理。

八、总结

  • useSyncExternalStore 是 React 18 为并发渲染设计的核心 Hook,通过安全订阅外部状态源,解决了状态与 UI 不一致的问题。
  • 它适用于需要与第三方状态管理库或浏览器 API 集成的场景,确保组件在并发渲染模式下仍能正确响应状态变化。并在不同浏览器标签页之间同步这些状态

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79496.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Dify框架面试内容整理-如何评估基于Dify开发的AI应用的效果?

评估基于 Dify 开发的 AI 应用效果,需要从 用户体验、技术性能 与 业务价值 三个层面综合衡量。以下是详细的评估框架,涵盖三个关键点: 用户反馈与满意度

Linux 系统下VS Code python环境配置!

Anaconda安装&#xff1a; 在 Linux 系统中安装下载好的 Anaconda3-2024.10-1-Linux-x86_64.sh&#xff0c;可按以下步骤操作&#xff1a; 1. 赋予安装脚本执行权限 打开终端&#xff0c;切换到安装包所在目录&#xff08;假设在 software 文件夹中&#xff09;&#xff0c;…

项目实战-基于信号处理与SVM机器学习的声音情感识别系统

目录 一.背景描述 二.理论部分 三.程序设计 编程思路 流程图 1.信号部分 创建数据 generate_samples.py 头文件 生成函数 generate_emotion_sample 传入参数 存储路径 生成参数 创建基础正弦波信号 调制基础正弦波 对于愤怒可以增加噪声 归一化信号 存储 主函…

虚幻引擎作者采访

1万小时编程_哔哩哔哩_bilibili https://www.youtube.com/watch?v477qF6QNSvc 提姆斯温尼是一位传奇性的视频游戏程序员&#xff0c;Epic Games 的创始人兼首席执行官。 该公司开发了虚幻引擎、堡垒之夜、战争机器、虚幻竞技场等许多开创性和有影响力的视频游戏。 他哥哥…

如何限制pod 进程/线程数量?

在 Kubernetes 中限制 Pod 的 进程数&#xff08;PID 数量&#xff09; 和 线程数&#xff0c;需要结合 Linux cgroup 控制 和 容器运行时配置。以下是具体方法和示例&#xff1a; 一、限制进程数&#xff08;PID 数量&#xff09; 1. 通过 pids cgroup 控制器限制 原理&…

使用 Hugging Face 镜像站快速下载大模型

在国内使用 Hugging Face 下载模型时&#xff0c;经常遇到连接慢、断点续传失败等问题。本文记录一个稳定、快速下载模型的命令行脚本&#xff0c;并支持设置模型缓存路径和目标目录&#xff0c;方便后续统一管理。 1. 设置 Hugging Face 镜像站 为了提升国内访问速度&#xf…

原语的使用

1、什么是原语&#xff1f;&#xff1f; 原语&#xff08; primitive &#xff09;&#xff0c;是FPGA开发环境所提供的一系列逻辑功能单元。往往与FPGA芯片的厂家精密相连&#xff0c;不同厂家的原语往往不能通用。 2、需要使用原语的情况 一般来说&#xff0c;在进行HDL cod…

大模型核心技术及架构解析

大模型核心技术及架构解析 大语言模型(Large Language Models, LLMs)已成为当前AI领域最重要的技术突破之一。以下是其核心技术和架构的全面分析&#xff1a; 一、核心技术组成 1. 基础架构技术 技术说明代表应用Transformer自注意力机制基础架构GPT, BERTMoE架构混合专家模…

ES6/ES11知识点 续三

rest参数 Rest 参数&#xff08;Rest Parameters&#xff09;是 ES6 引入的一个非常实用的特性。它允许函数接受不定数量的参数&#xff0c;并将这些参数作为一个数组存储&#xff0c;从而简化了处理可变参数的代码。 Rest 参数语法 Rest 参数使用 … 语法&#xff0c;紧跟着…

记忆翻牌游戏:认知科学与状态机的交响曲

目录 记忆翻牌游戏:认知科学与状态机的交响曲引言第一章 网格空间拓扑学1.1 自适应网格算法1.2 卡片排布原理第二章 状态机设计2.1 状态跃迁矩阵2.2 时空关联模型第三章 记忆强化机制3.1 认知衰减曲线3.2 注意力热力图第四章 动画引擎设计4.1 翻牌运动方程4.2 粒子反馈系统第五…

STM32外设-GPIO输出(不含复用)

STM32外设-GPIO输出&#xff08;不含复用&#xff09; 一&#xff0c;GPIO模式简介1&#xff0c;输入模式2&#xff0c;输出模式3&#xff0c;模拟模式4&#xff0c;复用模式 二&#xff0c;输出模式详解1&#xff0c; 输出类型1&#xff0c;推挽输出&#xff1a;2&#xff0c;…

58认知干货:创业经验分享及企业形式的汇总

机会永远都是留给有眼光、能发现机会的人,而不是留给有准备的人!往往机会就在身边,普罗大众却无法发现,而真正适合创业的人,天然具备这方面的能力。 当然后天的补足也未尝不可:“故常有欲以观其微,常无欲以观其妙。””引用《道德经》 读懂这句话自然便会拥有对商业和…

修复笔记:获取 torch._dynamo 的详细日志信息

一、问题描述 在运行项目时&#xff0c;遇到与 torch._dynamo 相关的报错&#xff0c;并且希望获取更详细的日志信息以便于进一步诊断问题。 二、相关环境变量设置 通过设置环境变量&#xff0c;可以获得更详细的日志信息&#xff1a; set TORCH_LOGSdynamo set TORCHDYNAM…

Spark,Idea中编写Spark程序 2

Idea中编写Spark程序 一、修改pom.xml文件 <build><sourceDirectory>src/main/scala</sourceDirectory><testSourceDirectory>src/test/scala</testSourceDirectory> <!-- 添加必要的插件以打包scala程序--><plugins><plu…

【AI提示词】黑天鹅模型专家

提示说明 详细解释黑天鹅模型的理论背景、定义、分类及其在不同领域的应用。 提示词 # Role: 黑天鹅模型专家## Profile - language: 中文 - description: 详细解释黑天鹅模型的理论背景、定义、分类及其在不同领域的应用 - background: 黑天鹅模型是尼尔斯莫尔提出的理论&a…

ARM Linux 设备树

Linux 设备驱动开发详解&#xff1a;基于最新的Linux 4.0内核, 机械工业出版社, 宋宝华, 2015 1. 设备树的起源 • 背景: ARM架构中大量板级代码冗余&#xff0c;硬编码在mach-xxx目录&#xff0c;设备树&#xff08;Device Tree&#xff09;引入结构化描述硬件。 • 目的: 减…

每日c/c++题 备战蓝桥杯(洛谷P1015 [NOIP 1999 普及组] 回文数)

洛谷P1015 [NOIP 1999 普及组] 回文数 题解 题目描述 P1015 回文数 是NOIP 1999普及组的经典模拟题。题目要求如下&#xff1a; 给定一个数N&#xff08;十进制&#xff09;和进制K&#xff08;2≤K≤16&#xff09;&#xff0c;将N转换为K进制表示后&#xff0c;通过以下操…

Linux线程深度解析:从基础到实践

Linux线程深度解析&#xff1a;从基础到实践 一、线程基础概念 1. 进程与线程定义 进程&#xff1a;一个正在运行的程序&#xff0c;是操作系统资源分配的最小单位&#xff08;拥有独立的地址空间、文件描述符等资源&#xff09;&#xff0c;状态包括就绪、运行、阻塞。线程…

php学习笔记(全面且适合新手)

以下是专为 PHP 7.4 初学者设计的全面学习文档&#xff0c;涵盖基础语法、细节语法和进阶语法&#xff0c;结合 PHP 7.4 新特性与实战案例&#xff0c;帮助系统掌握 PHP 开发&#xff1a; 为什么特地做7.4的笔记而不做8的&#xff1f;因为公司用的7.4&#xff0c;哈哈 一、基…

开源分布式数据库(TiDB)

TiDB是由PingCAP 开发的开源分布式数据库&#xff0c;兼容 MySQL 协议&#xff0c;集成了 HTAP&#xff08;混合事务和分析处理&#xff09;的能力&#xff0c;能够同时处理在线事务和实时分析任务。 2015 年&#xff0c;TiDB 在 GitHub 创建&#xff0c;2025 年&#xff0c;Ti…