useSyncExternalStore

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 中資料的快照。您需要傳遞兩個函式作為參數

  1. subscribe 函式應該訂閱 store 並返回一個取消訂閱的函式。
  2. 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 中資料的快照。您需要傳遞兩個函式作為參數

  1. subscribe 函式 應該訂閱 store 並返回一個取消訂閱的函式。
  2. 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>
    </>
  );
}

備註

如果可能,我們建議使用內建的 React state,搭配 useStateuseReducer 使用。useSyncExternalStore API 主要在您需要與現有的非 React 程式碼整合時才有用。


訂閱瀏覽器 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 物件上觸發 onlineoffline 事件。您需要將 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 並使其具有互動性時。

這讓您可以提供初始快照值,該值將在應用程式具有互動性之前使用。如果伺服器渲染沒有有意義的初始值,請省略傳遞給 強制在客戶端渲染 的參數。

備註

確保 getServerSnapshot 在初始客戶端渲染時傳回與伺服器上傳回的完全相同的資料。例如,如果 getServerSnapshot 在伺服器上傳回一些預先填入的儲存區內容,您需要將此內容傳輸到客戶端。一種方法是在伺服器渲染期間發出一個設定全域變數(例如 window.MY_STORE_DATA)的 <script> 標籤,並在客戶端的 getServerSnapshot 中從該全域變數讀取。您的外部儲存區應提供有關如何執行的說明。


問題排查

我收到一個錯誤:「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]);

// ...
}