React hook (useSyncExternalStore)

208 阅读4分钟

useSyncExternalStore

useSyncExternalStore 是一个让你订阅外部 store 的 React Hook。

作用:

1.订阅外部store

具体用法:

1.引入useSyncExternalStore在组件的顶层调用

import { useSyncExternalStore } from "react"

2.useSyncExternalStore接收两个参数,它返回 store 中数据的快照。

  • subscribe 函数应当订阅该 store 并返回一个取消订阅的函数。
  • getSnapshot 函数应当从该 store 读取数据的快照。

const res = useSyncExternalStore(subscribe, getSnapshot)

3.抽离逻辑,编写自定义Hook

import { useSyncExternalStore } from "react"
// 订阅浏览器API 例如(online,storage,location)等
// 抽离逻辑,编写自定义hooks

export const useStorage = (key: string, initValue: any) => {
    //订阅者
    const subscribe = (callback: () => void) => {
        window.addEventListener('storage', callback)
        //取消订阅
        return () => {
            window.removeEventListener('storage', callback)
        }
    }

    //返回当前快照
    const getSnapshot = () => {
        return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : initValue
    }

    const updateStorage = (value: any) => {
        localStorage.setItem(key, JSON.stringify(value))
        //手动触发Storage的事件 
        window.dispatchEvent(new StorageEvent('storage'))
    }
    const res = useSyncExternalStore(subscribe, a)
    return [res, updateStorage]
}

组件中使用

import { useStorage } from './hook/useStorage.ts'
const App = () => {
    const [count, setCount] = useStorage('count', 1)
    return (
        <>
            首页
            <div>
                <span>
                    {count}
                </span>
                <button onClick={() => setCount(count + 1)}>新增</button>
                <button onClick={() => setCount(count - 1)}>减少</button>
            </div>

        </>
    )
}
export default App

注意:

  • subscribe:一个函数,接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变时会调用提供的 callback,这将导致 React 重新调用 getSnapshot 并在需要的时候重新渲染组件。subscribe 函数会返回清除订阅的函数。
  • getSnapshot:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。
  • 可选 getServerSnapshot:一个函数,返回 store 中数据的初始快照。它只会在服务端渲染时,以及在客户端进行服务端渲染内容的激活时被用到。快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的。如果你忽略此参数,在服务端渲染这个组件会抛出一个错误。

在updateStorage这个函数中必须手动触发一下storage的事件,否则不会触发订阅者Callback执行,也就是你只能在localstorage中发现数据变化,但是页面数据是没有变化的。

警告:

zh-hans.react.dev/reference/r…

  • getSnapshot 返回的 store 快照必须是不可变的。如果底层 store 有可变数据,要在数据改变时返回一个新的不可变快照。否则,返回上次缓存的快照。
  • 如果在重新渲染时传入一个不同的 subscribe 函数,React 会用新传入的 subscribe 函数重新订阅该 store。你可以通过在组件外声明 subscribe 来避免。
  • 如果在 非阻塞 Transition 更新 过程中更改了 store,React 将会回退并将该更新视为阻塞更新。具体来说,在每次 Transition 更新时,React 将在将更改应用到 DOM 之前第二次调用 getSnapshot。如果它返回的值与最初调用时不同,React 将重新从头开始进行更新,这次将其作为阻塞更新应用,以确保屏幕上的每个组件都反映 store 的相同版本。
  • 不建议根据 useSyncExternalStore 返回的 store 值暂停渲染。原因是对外部 store 的变更无法 被标记为非阻塞 Transition 更新,因此它们会触发最近的 Suspense 后备方案,用加载旋转器替换已经呈现在屏幕上的内容,通常会导致较差的用户体验。

2.订阅浏览器api

除了订阅外部的store还可以订阅浏览器api 订阅当前的网络状态在页面显示

import { useSyncExternalStore } from "react";

export const useOnlineStatus = () => {
  const subscribe = (callback: () => void) => {
    window.addEventListener("online", callback);
    window.addEventListener("offline", callback);

    return () => {
      window.removeEventListener("online", callback);
      window.removeEventListener("offline", callback);
    };
  };
  const getSnapshot = () => {
    return navigator.onLine;
  };

  const update = () => {
    window.dispatchEvent(new Event("online"));
    window.dispatchEvent(new Event("offline"));
  };

  const res = useSyncExternalStore(subscribe, getSnapshot);
  return res;
};

组件中使用

import { useOnlineStatus } from './hook/useOnline.ts'
const App = () => {
    const [count, setCount] = useStorage('count', 1)
    const [url, push, replace] = useHistory()
    const onlineStatus = useOnlineStatus()
    return (
        <>
            <h1>网络状态{onlineStatus ? '在线' : '离线'}</h1>
        </>
           )
            ......
     }

image.png

性能问题:

“The result of `getSnapshot` should be cached” 出现这个报错说明你返回了新的对象

image.png 1.getSnapshot每次不要返回不同的对象,否则他会进入无线循环报错

2.如果每次的subscribe都不同,react会重新订阅store。如果你需要改变store可以使用useCallback,在某些参数发生变化时,然后重新订阅

3.添加服务端渲染支持

......后续更新