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>
</>
)
......
}
性能问题:
“The result of `getSnapshot` should be cached” 出现这个报错说明你返回了新的对象
1.getSnapshot每次不要返回不同的对象,否则他会进入无线循环报错
2.如果每次的subscribe都不同,react会重新订阅store。如果你需要改变store可以使用useCallback,在某些参数发生变化时,然后重新订阅
3.添加服务端渲染支持
......后续更新