useDeferredValue

useDeferredValue 是一個 React Hook,可讓你延遲更新 UI 的一部分。

const deferredValue = useDeferredValue(value)

參考

useDeferredValue(value, initialValue?)

在元件的最上層呼叫 useDeferredValue 以取得該值的延遲版本。

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

請參閱下面的更多範例。

參數

  • value:你想要延遲的值。它可以是任何類型。
  • 選用 initialValue:元件初始渲染期間要使用的值。如果省略此選項,useDeferredValue 將不會在初始渲染期間延遲,因為沒有它可以渲染的先前版本的 value

回傳值

  • currentValue:在初始渲染期間,返回的延遲值將是 initialValue,或者與您提供的值相同。在更新期間,React 將首先嘗試使用舊值重新渲染(因此它將返回舊值),然後在後台嘗試使用新值重新渲染(因此它將返回更新的值)。

注意事項

  • 當更新在 Transition 內時,useDeferredValue始終返回新的 value 並且不會產生延遲渲染,因為更新已被延遲。

  • 傳遞給 useDeferredValue 的值應為原始值(例如字串和數字)或在渲染過程外建立的物件。如果您在渲染期間建立一個新物件並立即將其傳遞給 useDeferredValue,它在每次渲染時都會不同,導致不必要的背景重新渲染。

  • useDeferredValue 收到不同的值(與 Object.is 比較)時,除了目前的渲染(仍然使用先前的值)之外,它還會在背景中使用新值安排重新渲染。背景重新渲染是可中斷的:如果 value 再次更新,React 將從頭開始重新啟動背景重新渲染。例如,如果使用者在輸入框中輸入的速度比接收其延遲值的圖表重新渲染的速度快,則圖表只會在使用者停止輸入後重新渲染。

  • useDeferredValue<Suspense> 整合。如果由新值引起的背景更新導致 UI 掛起,使用者將不會看到 fallback。他們會看到舊的延遲值,直到數據載入完成。

  • useDeferredValue 本身並不會阻止額外的網路請求。

  • useDeferredValue 本身沒有固定的延遲。一旦 React 完成初始重新渲染,React 將立即開始使用新的延遲值進行背景重新渲染。任何由事件(例如輸入)引起的更新都會中斷背景重新渲染,並優先於它。

  • useDeferredValue 引起的背景重新渲染在提交到螢幕之前不會觸發 Effects。如果背景重新渲染掛起,其 Effects 將在數據載入和 UI 更新後運行。


用法 ...

在載入新內容時顯示過時的內容 ...

在組件的頂層呼叫 useDeferredValue 以延遲更新 UI 的某些部分。

import { useState, useDeferredValue } from 'react';

function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}

在初始渲染期間,延遲值 將與您提供的 相同。

在更新期間,延遲值 將「落後」於最新的 。特別是,React 將首先重新渲染*而不*更新延遲值,然後嘗試在背景中使用新接收到的值重新渲染。

讓我們通過一個例子來看看它何時有用。

注意事項

此範例假設您使用支援 Suspense 的資料來源

  • 使用支援 Suspense 的框架(例如 RelayNext.js)進行數據提取
  • 使用 lazy 延遲載入組件程式碼
  • 使用 use 讀取 Promise 的值

深入瞭解 Suspense 及其限制。

在此範例中,SearchResults 組件在提取搜尋結果時 掛起。嘗試輸入 "a",等待結果,然後將其編輯為 "ab""a" 的結果將被載入 fallback 取代。

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

一種常見的替代 UI 模式是*延遲*更新結果列表,並在新的結果準備好之前繼續顯示先前的結果。呼叫 useDeferredValue 將查詢的延遲版本向下傳遞

export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}

query 將立即更新,因此輸入框將顯示新值。但是,deferredQuery 將保留其先前的值,直到數據載入完成,因此 SearchResults 將短暫顯示過時的結果。

在下面的範例中輸入 "a",等待結果載入,然後將輸入編輯為 "ab"。請注意,您現在看到的不是 Suspense fallback,而是過時的結果列表,直到新的結果載入完成

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

深入探討

延遲值的底層運作機制為何? ...

您可以將其視為分兩個步驟進行

  1. 首先,React 會使用新的 query"ab")和舊的 deferredQuery(仍然是 "a") 重新渲染。 你傳遞給結果列表的 deferredQuery 值是*延遲的*:它「落後於」query 值。

  2. 在背景中,React 會嘗試將 querydeferredQuery*都*更新為 "ab" 來重新渲染。 如果此重新渲染完成,React 將會在螢幕上顯示它。但是,如果它暫停("ab" 的結果尚未載入),React 將會放棄此渲染嘗試,並在資料載入後再次重試此重新渲染。使用者將會持續看到過時的延遲值,直到資料準備好為止。

延遲的「背景」渲染是可以中斷的。例如,如果你再次輸入文字到輸入框中,React 將會放棄它,並使用新的值重新開始。React 將永遠使用最新提供的值。

請注意,每次按鍵仍然會產生一個網路請求。這裡延遲的是顯示結果(直到它們準備好為止),而不是網路請求本身。即使使用者繼續輸入,每個按鍵的回應都會被快取,因此按下倒退鍵是即時的,而且不會再次發出請求。


指示內容已過時

在上面的例子中,沒有任何跡象表明最新查詢的結果列表仍在載入中。如果新的結果需要一段時間才能載入,這可能會讓使用者感到困惑。為了讓使用者更清楚地知道結果列表與最新的查詢不符,你可以在顯示過時的結果列表時新增視覺指示。

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>

透過這個更改,一旦你開始輸入,過時的結果列表就會稍微變暗,直到新的結果列表載入完成。你也可以新增 CSS 轉場效果來延遲變暗,使其感覺漸進,如下例所示。

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}


延遲重新渲染 UI 的一部分

你也可以將 useDeferredValue 作為效能優化的一種方式。當 UI 的一部分重新渲染速度緩慢,沒有簡單的方法可以優化它,並且你想防止它阻礙 UI 的其餘部分時,它會很有用。

想像你有一個文字欄位和一個在每次按鍵時都會重新渲染的組件(例如圖表或長列表)。

function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}

首先,優化 SlowList,使其在 props 相同時跳過重新渲染。要做到這一點,請將它包裝在 memo 中:

const SlowList = memo(function SlowList({ text }) {
// ...
});

但是,這只有在 SlowList 的 props 與前一次渲染時*相同*時才有幫助。你現在面臨的問題是,當它們*不同*時,以及當你實際需要顯示不同的視覺輸出時,它的速度會很慢。

具體來說,主要的效能問題是,每當你在輸入框中輸入文字時,SlowList 都會收到新的 props,而重新渲染它的整個樹狀結構會讓輸入感覺遲鈍。在這種情況下,useDeferredValue 可以讓你將更新輸入框(必須快速)的優先順序置於更新結果列表(允許較慢)之上。

function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}

這並不會讓 SlowList 的重新渲染速度更快。但是,它會告訴 React,可以降低列表重新渲染的優先順序,這樣它就不會阻礙按鍵輸入。列表將會「落後於」輸入框,然後「趕上」。和以前一樣,React 會嘗試盡快更新列表,但不會阻止使用者輸入。

useDeferredValue 與未優化重新渲染的差異

列表的延遲重新渲染範例 1 2:
列表的延遲重新渲染

在此範例中,SlowList 組件中的每個項目都*人為地降低了速度*,以便你可以看到 useDeferredValue 如何讓你保持輸入框的回應速度。在輸入框中輸入文字,你會注意到輸入感覺很快速,而列表則「落後」於它。

import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={deferredText} />
    </>
  );
}

陷阱

此優化需要將 SlowList 包裝在 memo 中。 這是因為每當 text 更改時,React 需要能夠快速重新渲染父組件。在該重新渲染期間,deferredText 仍然具有其先前的值,因此 SlowList 可以跳過重新渲染(其 props 沒有更改)。如果沒有 memo 它仍然必須重新渲染,這就失去了優化的意義。

深入探討

延遲值與防抖和節流有何不同?

在這種情況下,你可能之前使用過兩種常見的優化技術。

  • 防抖(Debouncing)指的是在使用者停止輸入一段時間後(例如一秒),才更新列表。
  • 節流(Throttling)指的是每隔一段時間(例如最多每秒一次)更新列表。

雖然這些技巧在某些情況下很有幫助,但 useDeferredValue 更適合優化渲染,因為它與 React 深度整合,並且會適應使用者的裝置。

與防抖或節流不同,它不需要選擇任何固定的延遲時間。如果使用者的裝置速度很快(例如效能強大的筆記型電腦),延遲的重新渲染幾乎會立即發生,而且不會被察覺。如果使用者的裝置速度很慢,列表會與輸入「延遲」的時間成正比,與裝置速度成反比。

此外,與防抖或節流不同,由 useDeferredValue 執行的延遲重新渲染預設是可以中斷的。這表示如果 React 正在重新渲染一個大型列表,但使用者又輸入了另一個按鍵,React 將會放棄該次重新渲染,處理按鍵輸入,然後再次開始背景渲染。相比之下,防抖和節流仍然會產生卡頓的體驗,因為它們是*阻塞性*的:它們只是延遲了渲染阻塞按鍵輸入的時刻。

如果您要優化的工作並非發生在渲染期間,防抖和節流仍然很有用。例如,它們可以讓您減少網路請求的次數。您也可以同時使用這些技巧。