React 中hooks之useSyncExternalStore使用总结

news2025/1/27 12:24:30

1. 基本概念

useSyncExternalStore 是 React 18 引入的一个 Hook,用于订阅外部数据源,确保在并发渲染下数据的一致性。它主要用于:

  • 订阅浏览器 API(如 window.width)
  • 订阅第三方状态管理库
  • 订阅任何外部数据源

1.1 基本语法

const state = useSyncExternalStore(
  subscribe,  // 订阅函数
  getSnapshot, // 获取当前状态的函数
  getServerSnapshot // 可选:服务端渲染时获取状态的函数
);

2. 基础示例

2.1 订阅窗口大小变化

getSnapshot 是一个函数,用于返回当前浏览器窗口的宽度和高度。window.innerWidth 和 window.innerHeight 分别获取浏览器窗口的宽度和高度。
该函数返回一个对象,包含 width 和 height 两个属性。
subscribe 函数接受一个回调函数 callback,并将其作为事件监听器绑定到 resize 事件上。
每当浏览器窗口的尺寸发生变化时,resize 事件会触发,进而调用 callback。
subscribe 函数还返回一个清理函数(return () => window.removeEventListener(‘resize’, callback)),用于在组件卸载时移除事件监听器,防止内存泄漏。
当callback回调触发的时候就会触发组件更新

function useWindowSize() {
  const getSnapshot = () => ({
    width: window.innerWidth,
    height: window.innerHeight
  });

  const subscribe = (callback) => {
    window.addEventListener('resize', callback);
    return () => window.removeEventListener('resize', callback);
  };

  return useSyncExternalStore(subscribe, getSnapshot);
}

function WindowSizeComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      Window size: {width} x {height}
    </div>
  );
}

2.2 订阅浏览器在线状态

function useOnlineStatus() {
  const getSnapshot = () => navigator.onLine;

  const subscribe = (callback) => {
    window.addEventListener('online', callback);
    window.addEventListener('offline', callback);
    
    return () => {
      window.removeEventListener('online', callback);
      window.removeEventListener('offline', callback);
    };
  };

  return useSyncExternalStore(subscribe, getSnapshot);
}

function OnlineStatusComponent() {
  const isOnline = useOnlineStatus();

  return (
    <div>
      Status: {isOnline ? '在线' : '离线'}
    </div>
  );
}

3. 进阶用法

3.1 创建自定义存储

useTodoStore 是一个自定义 Hook,它使用了 useSyncExternalStore 来同步外部存储(即 todoStore)的状态。
todoStore.subscribe:订阅状态更新。每当 todoStore 中的状态变化时,useSyncExternalStore 会触发重新渲染。
todoStore.getSnapshot:获取当前的状态快照,在此返回的对象包含 todos 和 filter。

function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();

  return {
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return state;
    },
    setState(newState) {
      state = newState;
      listeners.forEach(listener => listener());
    }
  };
}

const todoStore = createStore({
  todos: [],
  filter: 'all'
});

function useTodoStore() {
  return useSyncExternalStore(
    todoStore.subscribe,
    todoStore.getSnapshot
  );
}

function TodoList() {
  const { todos, filter } = useTodoStore();

  return (
    <ul>
      {todos
        .filter(todo => filter === 'all' || todo.completed === (filter === 'completed'))
        .map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
    </ul>
  );
}

3.2 与服务端渲染集成

function useSharedState(initialState) {
  const store = useMemo(() => createStore(initialState), [initialState]);

  // 提供服务端快照
  const getServerSnapshot = () => initialState;

  return useSyncExternalStore(
    store.subscribe,
    store.getSnapshot,
    getServerSnapshot
  );
}

3.3 订阅 WebSocket 数据

function useWebSocketData(url) {
  const [store] = useState(() => {
    let data = null;
    const listeners = new Set();
    
    const ws = new WebSocket(url);
    ws.onmessage = (event) => {
      data = JSON.parse(event.data);
      listeners.forEach(listener => listener());
    };

    return {
      subscribe(listener) {
        listeners.add(listener);
        return () => {
          listeners.delete(listener);
          if (listeners.size === 0) {
            ws.close();
          }
        };
      },
      getSnapshot() {
        return data;
      }
    };
  });

  return useSyncExternalStore(store.subscribe, store.getSnapshot);
}

function LiveDataComponent() {
  const data = useWebSocketData('wss://api.example.com/live');

  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <h2>实时数据</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

4. 性能优化

4.1 选择性订阅

function useStoreSelector(selector) {
  const store = useContext(StoreContext);
  
  const getSnapshot = useCallback(() => {
    return selector(store.getSnapshot());
  }, [store, selector]);

  return useSyncExternalStore(
    store.subscribe,
    getSnapshot
  );
}

// 使用示例
function TodoCounter() {
  const count = useStoreSelector(state => state.todos.length);
  return <div>Total todos: {count}</div>;
}

4.2 避免不必要的更新

function createStoreWithSelector(initialState) {
  let state = initialState;
  const listeners = new Map();

  return {
    subscribe(listener, selector) {
      const wrappedListener = () => {
        const newSelectedValue = selector(state);
        if (newSelectedValue !== selector(previousState)) {
          listener();
        }
      };
      listeners.set(listener, wrappedListener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return state;
    },
    setState(newState) {
      const previousState = state;
      state = newState;
      listeners.forEach(listener => listener());
    }
  };
}

5. 实际应用场景

5.1 主题切换系统

function createThemeStore() {
  let theme = 'light';
  const listeners = new Set();

  return {
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return theme;
    },
    toggleTheme() {
      theme = theme === 'light' ? 'dark' : 'light';
      listeners.forEach(listener => listener());
    }
  };
}

const themeStore = createThemeStore();

function useTheme() {
  return useSyncExternalStore(
    themeStore.subscribe,
    themeStore.getSnapshot
  );
}

function ThemeToggle() {
  const theme = useTheme();

  return (
    <button onClick={() => themeStore.toggleTheme()}>
      Current theme: {theme}
    </button>
  );
}

5.2 表单状态管理

function createFormStore(initialValues) {
  let values = initialValues;
  const listeners = new Set();

  return {
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot() {
      return values;
    },
    updateField(field, value) {
      values = { ...values, [field]: value };
      listeners.forEach(listener => listener());
    },
    reset() {
      values = initialValues;
      listeners.forEach(listener => listener());
    }
  };
}

function useForm(initialValues) {
  const [store] = useState(() => createFormStore(initialValues));
  
  return useSyncExternalStore(
    store.subscribe,
    store.getSnapshot
  );
}

function Form() {
  const formData = useForm({ name: '', email: '' });

  return (
    <form>
      <input
        value={formData.name}
        onChange={e => formStore.updateField('name', e.target.value)}
      />
      <input
        value={formData.email}
        onChange={e => formStore.updateField('email', e.target.value)}
      />
    </form>
  );
}

6. 注意事项

  1. 保持一致性

    • subscribe 函数应该返回清理函数
    • getSnapshot 应该返回不可变的数据
  2. 避免频繁更新

    • 考虑使用节流或防抖
    • 实现选择性订阅机制
  3. 服务端渲染

    • 提供 getServerSnapshot
    • 确保服务端和客户端状态同步
  4. 内存管理

    • 及时清理订阅
    • 避免内存泄漏

通过合理使用 useSyncExternalStore,我们可以安全地订阅外部数据源,并确保在 React 并发渲染下的数据一致性。这个 Hook 特别适合需要与外部系统集成的场景。 还可以用来实现浏览器localStrorage的持久化存储

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

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

相关文章

leetcode刷题记录(八十九)——35. 搜索插入位置

&#xff08;一&#xff09;问题描述 35. 搜索插入位置 - 力扣&#xff08;LeetCode&#xff09;35. 搜索插入位置 - 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位…

Python Typing: 实战应用指南

文章目录 1. 什么是 Python Typing&#xff1f;2. 实战案例&#xff1a;构建一个用户管理系统2.1 项目描述2.2 代码实现 3. 类型检查工具&#xff1a;MyPy4. 常见的 typing 用法5. 总结 在 Python 中&#xff0c;静态类型检查越来越受到开发者的重视。typing 模块提供了一种方式…

Linux之Tcp粘包笔记

目录 一.网络传输四层模型 二.数据传输中数据包传输的两个限制概念 三.数据传输的中粘包问题 四.数据组装的原因 Nagle算法原理: 五.关闭Nagle优化处理粘包问题吗&#xff1f; 六.粘包处理方法 a.设置消息边界&#xff1a; b.定义消息长度&#xff1a; 七.UDP是否会出…

22_解析XML配置文件_List列表

解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…

数据结构 链表2

目录 前言&#xff1a; 一&#xff0c;反转一个链表(迭代) 二&#xff0c;打印一个链表&#xff08;递归&#xff09; 三&#xff0c;反转一个链表(递归) 四&#xff0c;双向链表 总结 前言&#xff1a; 我们根据 [文章 链表1] 可以知道链表相比较于数组的优缺点和计算机…

Linux查看服务器的内外网地址

目录&#xff1a; 1、内网地址2、外网地址3、ping时显示地址与真实不一致 1、内网地址 ifconfig2、外网地址 curl ifconfig.me3、ping时显示地址与真实不一致 原因是dns缓存导致的&#xff0c;ping这种方法也是不准确的&#xff0c;有弊端不建议使用&#xff0c;只适用于测试…

【kong gateway】5分钟快速上手kong gateway

kong gateway的请求响应示意图 安装 下载对应的docker 镜像 可以直接使用docker pull命令拉取&#xff0c;也可以从以下地址下载&#xff1a;kong gateway 3.9.0.0 docker 镜像 https://download.csdn.net/download/zhangshenglu1/90307400&#xff0c; postgres-13.tar http…

缓存商品、购物车(day07)

缓存菜品 问题说明 问题说明&#xff1a;用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大。 结果&#xff1a; 系统响应慢、用户体验差 实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询…

Langchain+讯飞星火大模型Spark Max调用

1、安装langchain #安装langchain环境 pip install langchain0.3.3 openai -i https://mirrors.aliyun.com/pypi/simple #灵积模型服务 pip install dashscope -i https://mirrors.aliyun.com/pypi/simple #安装第三方集成,就是各种大语言模型 pip install langchain-comm…

八股学习 微服务篇

微服务篇 常见面试内容Spring Cloud 常见组件注册中心Ribbon负载均衡策略服务雪崩 常见面试内容 Spring Cloud 常见组件 Spring Cloud有5个常见组件&#xff1a; Eureka/Nacos:注册中心&#xff1b;Ribbon:负载均衡&#xff1b;Feign:远程调用&#xff1b;Hystrix/Sentinel:服…

【xcode 16.2】升级xcode后mac端flutter版的sentry报错

sentry_flutter 7.11.0 报错 3 errors in SentryCrashMonitor_CPPException with the errors No type named terminate_handler in namespace std (line 60) and No member named set_terminate in namespace std 替换sentry_flutter版本为&#xff1a; 8.3.0 从而保证oc的…

electron打包客户端在rk3588上支持h265硬解

目录 前言 chromium是如何支持h265硬解 electron/chromium第一次编译 electron/chromium第二次编译 前言 我们的客户端程序是用electron打包的前端程序&#xff0c;其在rk3588主机上的linux环境运行。之前使用客户端查看h264编码的视频直播是没有问题的&#xff0c;但视频源…

基于物联网的风机故障检测装置的设计与实现

1 系统总体设计方案 通过对风机故障检测装置的设计与实现的需求、可行性进行分析&#xff0c;本设计风机故障检测装置的设计与实现的系统总体架构设计如图2-1所示&#xff0c;系统风机故障检测装置采用STM32F103单片机作为控制器&#xff0c;并通过DS18B20温度传感器、ACS712电…

为什么IDEA提示不推荐@Autowired❓️如果使用@Resource呢❓️

前言 在使用 Spring 框架时&#xff0c;依赖注入&#xff08;DI&#xff09;是一个非常重要的概念。通过注解&#xff0c;我们可以方便地将类的实例注入到其他类中&#xff0c;提升开发效率。Autowired又是被大家最为熟知的方式&#xff0c;但很多开发者在使用 IntelliJ IDEA …

软件工程的概论

软件的概念与特点 软件的定义 软件 程序 数据 文档 软件的特征 1。软件是人开发的 2。软件生产是简单的拷贝 3。软件会多次生产 4。软件开发环境对产品影响很大 5。 软件的双重作用 一方面是一种产品另一方面是开发其他软件产品的工具。 软件分类 按软件功能&…

1. 握手问题python解法——2024年省赛蓝桥杯真题

原题传送门&#xff1a;1.握手问题 - 蓝桥云课 问题描述 小蓝组织了一场算法交流会议&#xff0c;总共有 50人参加了本次会议。在会议上&#xff0c;大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手 (且仅有一次)。但有 7 个人&#xff0c;…

【Uniapp-Vue3】setTabBar设置TabBar和下拉刷新API

一、setTabBar设置 uni.setTabBarItem({ index:"需要修改第几个", text:"修改后的文字内容" }) 二、tabBar的隐藏和显式 // 隐藏tabBar uni.hideTabBar(); // 显示tabBar uni.showTabBar(); 三、为tabBar右上角添加文本 uni.setTabBarBadge({ index:"…

Visual Studio Code修改terminal字体

个人博客地址&#xff1a;Visual Studio Code修改terminal字体 | 一张假钞的真实世界 默认打开中断后字体显示如下&#xff1a; 打开设置&#xff0c;搜索配置项terminal.integrated.fontFamily&#xff0c;修改配置为monospace。修改后效果如下&#xff1a;

使用ArcMap或ArcGIS Pro连接达梦数据库创建空间数据库

一、ArcMap 1、本地windows安装 32 位 DM 数据库客户端 2、覆盖dll 将 32 位 DM 数据的..\dmdbms\bin 目录中的 .dll 文件全部拷贝到 ArcGIS 的 ..\Desktop10.5\bin 目录下&#xff0c;有同名文件直接覆盖掉 3、开启达梦数据库空间扩展支持 使用管理员用户登录数据&#xff…

案例研究丨浪潮云洲通过DataEase推进多维度数据可视化建设

浪潮云洲工业互联网有限公司&#xff08;以下简称为“浪潮云洲”&#xff09;成立于2018年&#xff0c;定位于工业数字基础设施建设商、具有国际影响力的工业互联网平台运营商、生产性互联网头部服务商。截至目前&#xff0c;浪潮云洲工业互联网平台连续五年入选跨行业跨领域工…