一、背景
- useSyncExternalStore 是 React 18 引入的一个 Hook;
- 用于从外部存储(例如状态管理库、浏览器 API 等)获取状态并在组件中同步显示。这对于需要跟踪外部状态的应用非常有用。
二、场景
- 订阅外部 store 例如(redux,mobx,Zustand,jotai) vue的 vuex pinia
- 订阅浏览器API 例如(online,storage,location, history hash)等
- 抽离逻辑,编写自定义hooks
- 服务端渲染支持
三、用法
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
)
- 我们实现一个useStorage Hook,用于订阅 localStorage 数据。这样做的好处是,我们可以确保组件在 localStorage 数据发生变化时,自动更新同步。
- 我们将创建一个 useStorage Hook,能够存储数据到 localStorage,并在不同浏览器标签页之间同步这些状态。
- 此 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,实现基本的前进、后退操作以及程序化导航。
效果演示
- history:这是 useHistory 返回的当前路径值。每次 URL 变化时,useSyncExternalStore 会自动触发更新,使 history 始终保持最新路径。
- push 和 replace:
- 点击“跳转”按钮调用 push(“/push”),会将 /push推入历史记录;
- 点击“替换”按钮调用 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;
六、注意事项
-
避免条件渲染:不应基于
useSyncExternalStore
返回的状态值进行条件渲染(如动态加载懒加载组件),因为外部状态变化无法被标记为非阻塞更新,可能触发 Suspense 后备方案,导致用户体验不佳。 -
不可变快照:
getSnapshot
返回的快照必须是不可变的。若底层状态变化,需返回新的不可变快照。 -
清理订阅:
subscribe
函数需返回清理函数,确保组件卸载时取消订阅,防止内存泄漏。 -
如果
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
- 并发渲染安全性:
useEffect
+useState
在并发模式下可能导致旧值问题,而useSyncExternalStore
通过同步读取快照确保状态一致性。 - 适用场景:
useSyncExternalStore
更适合需要安全订阅外部状态源的场景,而useEffect
+useState
适用于简单的状态管理。
八、总结
useSyncExternalStore
是 React 18 为并发渲染设计的核心 Hook,通过安全订阅外部状态源,解决了状态与 UI 不一致的问题。- 它适用于需要与第三方状态管理库或浏览器 API 集成的场景,确保组件在并发渲染模式下仍能正确响应状态变化。并在不同浏览器标签页之间同步这些状态。