useSyncExternalStore
是一個 React Hook,可讓您訂閱外部 store。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
參考
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
在元件的頂層呼叫 useSyncExternalStore
來從外部資料 store 讀取值。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
它會返回 store 中資料的快照。您需要傳遞兩個函式作為參數
subscribe
函式應該訂閱 store 並返回一個取消訂閱的函式。getSnapshot
函式應該從 store 讀取資料的快照。
參數
-
subscribe
:一個函式,它接受單個callback
參數並將其訂閱到 store。當 store 更改時,它應該呼叫提供的callback
,這將導致 React 重新呼叫getSnapshot
並(如果需要)重新渲染元件。subscribe
函式應該返回一個清除訂閱的函式。 -
getSnapshot
:一個函式,它返回元件所需的 store 中資料的快照。當 store 未更改時,重複呼叫getSnapshot
必須返回相同的值。如果 store 更改且返回值不同(與Object.is
比較),React 會重新渲染元件。 -
選用
getServerSnapshot
:一個函式,它返回 store 中資料的初始快照。它僅在伺服器渲染期間和客戶端上伺服器渲染內容的水合期間使用。伺服器快照在客戶端和伺服器之間必須相同,並且通常會被序列化並從伺服器傳遞到客戶端。如果您省略此參數,在伺服器上渲染元件將會拋出錯誤。
返回
您可以在渲染邏輯中使用的 store 的當前快照。
注意事項
-
getSnapshot
返回的 store 快照必須是不可 -
如果在重新渲染期間傳遞了不同的
subscribe
函式,React 將使用新傳遞的subscribe
函式重新訂閱 store。您可以透過在元件外部宣告subscribe
來防止這種情況。 -
如果在非阻塞式 Transition 更新期間 store 發生變異,React 將會退回執行該更新為阻塞式。具體來說,對於每個 Transition 更新,React 會在將變更套用到 DOM 之前再次呼叫
getSnapshot
。如果它返回的值與最初呼叫時不同,React 將從頭開始重新啟動更新,這次將其作為阻塞式更新套用,以確保螢幕上的每個元件都反映 store 的相同版本。 -
不建議根據
useSyncExternalStore
返回的 store 值來*暫停*渲染。原因是對外部 store 的變異無法標記為非阻塞式 Transition 更新,因此它們將觸發最近的Suspense
後備,將螢幕上已渲染的內容替換為載入指示器,這通常會導致糟糕的使用者體驗。例如,以下用法是不建議的
const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));function ShoppingApp() {const selectedProductId = useSyncExternalStore(...);// ❌ Calling `use` with a Promise dependent on `selectedProductId`const data = use(fetchItem(selectedProductId))// ❌ Conditionally rendering a lazy component based on `selectedProductId`return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;}
用法...
訂閱外部 store...
大多數 React 元件只會從它們的 props、state、和 context 讀取資料。但是,有時元件需要從 React 外部的一些 store 讀取一些會隨著時間變化的資料。這包括
- 在 React 外部保存狀態的第三方狀態管理函式庫。
- 公開可變值和事件以訂閱其變化的瀏覽器 API。
在元件的頂層呼叫 useSyncExternalStore
來從外部資料 store 讀取值。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
它返回 store 中資料的快照。您需要傳遞兩個函式作為參數
subscribe
函式 應該訂閱 store 並返回一個取消訂閱的函式。getSnapshot
函式 應該從 store 中讀取資料的快照。
React 將使用這些函式來保持您的元件訂閱 store 並在變更時重新渲染它。
例如,在下面的沙盒中,todosStore
作為一個外部 store 實現,用於儲存在 React 外部的資料。TodosApp
元件使用 useSyncExternalStore
Hook 連接到該外部 store。
import { useSyncExternalStore } from 'react'; import { todosStore } from './todoStore.js'; export default function TodosApp() { const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot); return ( <> <button onClick={() => todosStore.addTodo()}>Add todo</button> <hr /> <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); }
訂閱瀏覽器 API...
新增 useSyncExternalStore
的另一個原因是,當您想要訂閱瀏覽器公開的一些會隨著時間變化的值時。例如,假設您希望您的元件顯示網路連線是否處於活動狀態。瀏覽器透過名為 navigator.onLine
的屬性公開此資訊。
這個值可能會在 React 不知情的情況下發生變化,因此您應該使用 useSyncExternalStore
來讀取它。
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
要實作 getSnapshot
函式,請從瀏覽器 API 讀取目前值
function getSnapshot() {
return navigator.onLine;
}
接下來,您需要實作 subscribe
函式。例如,當 navigator.onLine
變更時,瀏覽器會在 window
物件上觸發 online
和 offline
事件。您需要將 callback
參數訂閱到相應的事件,然後返回一個清除訂閱的函式
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
現在 React 知道如何從外部的 navigator.onLine
API 讀取值,以及如何訂閱其變更。將您的裝置與網路斷開連線,您會注意到元件會重新渲染以回應變更。
import { useSyncExternalStore } from 'react'; export default function ChatIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; } function getSnapshot() { return navigator.onLine; } function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; }
將邏輯提取到自定義 Hook
通常您不會直接在元件中撰寫 useSyncExternalStore
。相反地,您通常會從您自己的自定義 Hook 中呼叫它。這讓您可以從不同的元件使用相同的外部儲存區。
例如,這個自定義的 useOnlineStatus
Hook 會追蹤網路是否連線。
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
現在不同的元件可以呼叫 useOnlineStatus
而無需重複底層的實作。
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ Progress saved'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'Save progress' : 'Reconnecting...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
新增伺服器渲染支援
如果您的 React 應用程式使用 伺服器渲染,您的 React 元件也將在瀏覽器環境之外運行以產生初始 HTML。這在連接到外部儲存區時會產生一些挑戰。
- 如果您連接到僅限瀏覽器的 API,它將無法運作,因為它在伺服器上不存在。
- 如果您連接到第三方資料儲存區,您需要其資料在伺服器和客戶端之間保持一致。
要解決這些問題,請將 getServerSnapshot
函式作為第三個參數傳遞給 useSyncExternalStore
。
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // Always show "Online" for server-generated HTML
}
function subscribe(callback) {
// ...
}
getServerSnapshot
函式類似於 getSnapshot
,但它僅在兩種情況下運行:
- 它在伺服器上產生 HTML 時運行。
- 它在客戶端進行 注水(hydration) 時運行,也就是 React 取得伺服器 HTML 並使其具有互動性時。
這讓您可以提供初始快照值,該值將在應用程式具有互動性之前使用。如果伺服器渲染沒有有意義的初始值,請省略傳遞給 強制在客戶端渲染 的參數。
問題排查
我收到一個錯誤:「getSnapshot
的結果應該被快取」
這個錯誤表示您的 getSnapshot
函式每次被呼叫時都會傳回一個新的物件,例如:
function getSnapshot() {
// 🔴 Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}
如果 getSnapshot
的傳回值與上次不同,React 將會重新渲染元件。這就是為什麼如果您總是傳回不同的值,您將進入無限迴圈並收到此錯誤。
您的 getSnapshot
物件應該只在實際發生變更時才傳回不同的物件。如果您的儲存區包含不可變的資料,您可以直接傳回該資料。
function getSnapshot() {
// ✅ You can return immutable data
return myStore.todos;
}
如果您的儲存區資料是可變的,您的 getSnapshot
函式應該傳回其不可變的快照。這表示它*確實*需要建立新的物件,但它不應該每次呼叫都這樣做。相反地,它應該儲存上次計算的快照,如果儲存區中的資料沒有變更,則傳回與上次相同的快照。如何判斷可變資料是否已變更取決於您的可變儲存區。
我的 subscribe
函式在每次重新渲染後都被呼叫
這個 subscribe
函式是在元件*內部*定義的,因此每次重新渲染時它都是不同的。
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
// ...
}
如果您在重新渲染之間傳遞不同的 subscribe
函式,React 將會重新訂閱您的儲存區。如果這造成效能問題,並且您想要避免重新訂閱,請將 subscribe
函式移到外部。
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
或者,將 subscribe
包裝到 useCallback
中,以便僅在某些參數變更時才重新訂閱。
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}