学习React-9-useSyncExternalStore

useSyncExternalStore

useSyncExternalStore 是 React 18 新增 的 Hook,专门用来把React 外部的可变数据源安全地同步到组件内部,并且天然支持并发渲染(Concurrent Features)。一句话:只要状态不在 React 内部管理(localStorage、全局变量、Redux store、WebSocket 等),就用它来做订阅-渲染桥梁。

签名三要素
ts 复制代码
const subscribe = (callback: () => void) => {
    // 订阅
    callback() 
    return () => { 
        // 取消订阅
    }
}
// 返回当前快照
const getSnapshot = () => {
    return data
}

const snapshot = useSyncExternalStore(
  subscribe,          // (onStoreChange) => () => void   订阅+退订
  getSnapshot,        // () => Snapshot                  读取当前快照
  getServerSnapshot?  // () => Snapshot                  SSR 时的初始值
);
  • subscribe:组件挂载时 React 会调用它注册监听器;返回的函数会在卸载时执行做清理
  • getSnapshot:每次渲染前 React 会调用它拿到最新状态,并用 Object.is上一次快照比较,决定要不要重新渲染
  • getServerSnapshot(可选):服务端渲染时给 React 一个不会报错的初始值,避免hydration不匹配
对比
场景 useState/useEffect 写法 useSyncExternalStore 优势
localStorage 主题切换 需判断 typeof window,监听 storage 事件,同页修改无事件触发,SSR 报错 天生 SSR 安全,同页更新无压力
Redux/Zustand 手写 useEffect 订阅、比较引用,并发模式下易出现 tearing(同一时刻不同组件读到不同值) 官方保证 tear-free,并发安全
全局变量/WebSocket 手动 setState 触发更新,易重复渲染或漏更新 精准订阅,按需渲染
案例

1. 订阅浏览器Api 实现自定义hook(useStorage)

复制代码
 需求:实现一个useStorage Hook,用于订阅 localStorage 数据。
 作用:确保组件在 localStorage 数据发生变化时,自动更新同步。

实现思路:创建一个 useStorage Hook,能够存储数据到 localStorage,并在不同浏览器标签页之间同步这些状态。此 Hook 接收一个键值参数用于存储数据的键名,还可以接收一个默认值用于在无数据时的初始化。

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

ts 复制代码
import { useSyncExternalStore } from "react"

export default function useStorage(key: string, initialValue: any) {

    // 订阅者
    const subscribe = (callback: () => void) => {
        // 订阅浏览器Api
        window.addEventListener('storage', callback)
        return () => {
            // 取消订阅
            window.removeEventListener('storage', callback)
        }
    }

    // 获取当前数据源的快照
    const getSnapshot = () => {
        const item = localStorage.getItem(key)
        return item ? JSON.parse(item) : initialValue
    }

    // useSyncExternalStore(订阅者, 当前数据源的快照)
    const res = useSyncExternalStore(subscribe, getSnapshot)
    // 更新缓存
    const updateStorage = (value: any) => {
        localStorage.setItem(key, JSON.stringify(value))
        // 手动触发storage事件
        window.dispatchEvent(new StorageEvent('storage'))
    }
    return [res, updateStorage]
}

在App.tsx中使用:

ts 复制代码
import useStorage from "./hooks/useStorage"
function App() {
  const [count, setCount] = useStorage('count', 0)
  return (
  		<button onClick={() => setCount(count + 1)}> + </button>
        Index: {count}
        <button onClick={() => setCount(count - 1)}> - </button>

    </>
  )
}
export default App

效果展示:
2. 订阅history实现路由跳转

复制代码
需求:实现一个简易的useHistory Hook,获取浏览器url信息 + 参数

useHistory.tsx

ts 复制代码
import { useSyncExternalStore } from "react"

export const useHistory = () => {
    const subscribe = (callback: () => void) => { 
        // 订阅浏览器Api
        // vue 中路由三种模式, ssr使用、 两种web: history, hash 
        // history 监听url变化用popstate
        // hash 监听url变化用hashchange

        window.addEventListener('popstate', callback)
        window.addEventListener('hashchange', callback)
        return () => {
            // 取消订阅
           window.removeEventListener('popstate', callback)
           window.removeEventListener('hashchange', callback)
        }
        // postate 只能监听浏览器前进后退,无法监听pushState和replaceState. 需要手动触发
    }

    // 获取快照
    const getSnapshot = () => {
        return window.location.href
    }

    const url = useSyncExternalStore(subscribe, getSnapshot)

    const push = (url: string) => {
        window.history.pushState({}, '', url)
        window.dispatchEvent(new PopStateEvent('popstate'))
    }
    const replace = (url: string) => {
        window.history.replaceState({}, '', url)
        window.dispatchEvent(new PopStateEvent('popstate'))
    }

    return [url, push, replace] as const // 定义成元组类型  

    // 注意: [1, '3', false] => 这的值会被推断成联合类型 number | string | boolean. 元组值是元组类型,则不会被推断成联合类型
} 

在App.tsx使用:

ts 复制代码
import { useHistory } from "./hooks/useHistory"
function App() {
  const [count, setCount] = useStorage('count', 0)
  return (
  		<h1>url: {url}</h1>
        <button onClick={() => push('/A')}>push</button>
        <button onClick={() => replace('/B')}>push</button>
    </>
  )
}
export default App

实现效果:

总结
  • React 之外的状态 → useSyncExternalStore
  • React 之内的状态 → useState / useReducer / useContext
相关推荐
~无忧花开~3 分钟前
CSS学习笔记(五):CSS媒体查询入门指南
开发语言·前端·css·学习·媒体
吴鹰飞侠18 分钟前
AJAX的学习
前端·学习·ajax
JNU freshman24 分钟前
vue 技巧与易错
前端·javascript·vue.js
Asort32 分钟前
JavaScript设计模式(十六)——迭代器模式:优雅遍历数据的艺术
前端·javascript·设计模式
我是日安41 分钟前
从零到一打造 Vue3 响应式系统 Day 28 - shallowRef、shallowReactive
前端·javascript·vue.js
开源之眼43 分钟前
深入理解 JavaScript 报错:TypeError: undefined is not a function
前端·javascript
LRH43 分钟前
时间切片 + 双工作循环 + 优先级模型:React 的并发任务管理策略
前端·react.js
却尘1 小时前
当你敲下 `pnpm run dev`,这台机器到底在背后干了什么?
前端·javascript·面试
歪歪1001 小时前
React Native开发有哪些优势和劣势?
服务器·前端·javascript·react native·react.js·前端框架
我的xiaodoujiao1 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 19--测试框架Pytest基础 3--前后置操作应用
python·学习·测试工具·pytest