1. 基本概念
useSyncExternalStore 是 React 18 引入的一個 Hook,用于訂閱外部數據源,確保在并發渲染下數據的一致性。它主要用于:
- 訂閱瀏覽器 API(如 window.width)
- 訂閱第三方狀態管理庫
- 訂閱任何外部數據源
1.1 基本語法
const state = useSyncExternalStore(subscribe, // 訂閱函數getSnapshot, // 獲取當前狀態的函數getServerSnapshot // 可選:服務端渲染時獲取狀態的函數
);
2. 基礎示例
2.1 訂閱窗口大小變化
getSnapshot 是一個函數,用于返回當前瀏覽器窗口的寬度和高度。window.innerWidth 和 window.innerHeight 分別獲取瀏覽器窗口的寬度和高度。
該函數返回一個對象,包含 width 和 height 兩個屬性。
subscribe 函數接受一個回調函數 callback,并將其作為事件監聽器綁定到 resize 事件上。
每當瀏覽器窗口的尺寸發生變化時,resize 事件會觸發,進而調用 callback。
subscribe 函數還返回一個清理函數(return () => window.removeEventListener(‘resize’, callback)),用于在組件卸載時移除事件監聽器,防止內存泄漏。
當callback回調觸發的時候就會觸發組件更新
function useWindowSize() {const getSnapshot = () => ({width: window.innerWidth,height: window.innerHeight});const subscribe = (callback) => {window.addEventListener('resize', callback);return () => window.removeEventListener('resize', callback);};return useSyncExternalStore(subscribe, getSnapshot);
}function WindowSizeComponent() {const { width, height } = useWindowSize();return (<div>Window size: {width} x {height}</div>);
}
2.2 訂閱瀏覽器在線狀態
function useOnlineStatus() {const getSnapshot = () => navigator.onLine;const subscribe = (callback) => {window.addEventListener('online', callback);window.addEventListener('offline', callback);return () => {window.removeEventListener('online', callback);window.removeEventListener('offline', callback);};};return useSyncExternalStore(subscribe, getSnapshot);
}function OnlineStatusComponent() {const isOnline = useOnlineStatus();return (<div>Status: {isOnline ? '在線' : '離線'}</div>);
}
3. 進階用法
3.1 創建自定義存儲
useTodoStore 是一個自定義 Hook,它使用了 useSyncExternalStore 來同步外部存儲(即 todoStore)的狀態。
todoStore.subscribe:訂閱狀態更新。每當 todoStore 中的狀態變化時,useSyncExternalStore 會觸發重新渲染。
todoStore.getSnapshot:獲取當前的狀態快照,在此返回的對象包含 todos 和 filter。
function createStore(initialState) {let state = initialState;const listeners = new Set();return {subscribe(listener) {listeners.add(listener);return () => listeners.delete(listener);},getSnapshot() {return state;},setState(newState) {state = newState;listeners.forEach(listener => listener());}};
}const todoStore = createStore({todos: [],filter: 'all'
});function useTodoStore() {return useSyncExternalStore(todoStore.subscribe,todoStore.getSnapshot);
}function TodoList() {const { todos, filter } = useTodoStore();return (<ul>{todos.filter(todo => filter === 'all' || todo.completed === (filter === 'completed')).map(todo => (<li key={todo.id}>{todo.text}</li>))}</ul>);
}
3.2 與服務端渲染集成
function useSharedState(initialState) {const store = useMemo(() => createStore(initialState), [initialState]);// 提供服務端快照const getServerSnapshot = () => initialState;return useSyncExternalStore(store.subscribe,store.getSnapshot,getServerSnapshot);
}
3.3 訂閱 WebSocket 數據
function useWebSocketData(url) {const [store] = useState(() => {let data = null;const listeners = new Set();const ws = new WebSocket(url);ws.onmessage = (event) => {data = JSON.parse(event.data);listeners.forEach(listener => listener());};return {subscribe(listener) {listeners.add(listener);return () => {listeners.delete(listener);if (listeners.size === 0) {ws.close();}};},getSnapshot() {return data;}};});return useSyncExternalStore(store.subscribe, store.getSnapshot);
}function LiveDataComponent() {const data = useWebSocketData('wss://api.example.com/live');if (!data) return <div>Loading...</div>;return (<div><h2>實時數據</h2><pre>{JSON.stringify(data, null, 2)}</pre></div>);
}
4. 性能優化
4.1 選擇性訂閱
function useStoreSelector(selector) {const store = useContext(StoreContext);const getSnapshot = useCallback(() => {return selector(store.getSnapshot());}, [store, selector]);return useSyncExternalStore(store.subscribe,getSnapshot);
}// 使用示例
function TodoCounter() {const count = useStoreSelector(state => state.todos.length);return <div>Total todos: {count}</div>;
}
4.2 避免不必要的更新
function createStoreWithSelector(initialState) {let state = initialState;const listeners = new Map();return {subscribe(listener, selector) {const wrappedListener = () => {const newSelectedValue = selector(state);if (newSelectedValue !== selector(previousState)) {listener();}};listeners.set(listener, wrappedListener);return () => listeners.delete(listener);},getSnapshot() {return state;},setState(newState) {const previousState = state;state = newState;listeners.forEach(listener => listener());}};
}
5. 實際應用場景
5.1 主題切換系統
function createThemeStore() {let theme = 'light';const listeners = new Set();return {subscribe(listener) {listeners.add(listener);return () => listeners.delete(listener);},getSnapshot() {return theme;},toggleTheme() {theme = theme === 'light' ? 'dark' : 'light';listeners.forEach(listener => listener());}};
}const themeStore = createThemeStore();function useTheme() {return useSyncExternalStore(themeStore.subscribe,themeStore.getSnapshot);
}function ThemeToggle() {const theme = useTheme();return (<button onClick={() => themeStore.toggleTheme()}>Current theme: {theme}</button>);
}
5.2 表單狀態管理
function createFormStore(initialValues) {let values = initialValues;const listeners = new Set();return {subscribe(listener) {listeners.add(listener);return () => listeners.delete(listener);},getSnapshot() {return values;},updateField(field, value) {values = { ...values, [field]: value };listeners.forEach(listener => listener());},reset() {values = initialValues;listeners.forEach(listener => listener());}};
}function useForm(initialValues) {const [store] = useState(() => createFormStore(initialValues));return useSyncExternalStore(store.subscribe,store.getSnapshot);
}function Form() {const formData = useForm({ name: '', email: '' });return (<form><inputvalue={formData.name}onChange={e => formStore.updateField('name', e.target.value)}/><inputvalue={formData.email}onChange={e => formStore.updateField('email', e.target.value)}/></form>);
}
6. 注意事項
-
保持一致性
- subscribe 函數應該返回清理函數
- getSnapshot 應該返回不可變的數據
-
避免頻繁更新
- 考慮使用節流或防抖
- 實現選擇性訂閱機制
-
服務端渲染
- 提供 getServerSnapshot
- 確保服務端和客戶端狀態同步
-
內存管理
- 及時清理訂閱
- 避免內存泄漏
通過合理使用 useSyncExternalStore,我們可以安全地訂閱外部數據源,并確保在 React 并發渲染下的數據一致性。這個 Hook 特別適合需要與外部系統集成的場景。 還可以用來實現瀏覽器localStrorage的持久化存儲