一、背景
- 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 集成的場景,確保組件在并發渲染模式下仍能正確響應狀態變化。并在不同瀏覽器標簽頁之間同步這些狀態。