useDeferredValue

useDeferredValue - це хук React, який дозволяє відкласти оновлення частини інтерфейсу користувача.

const deferredValue = useDeferredValue(value)

Довідник

useDeferredValue(value)

Викличте useDeferredValue на верхньому рівні вашого компонента, щоб отримати відкладений варіант цього значення.

import { useState, useDeferredValue } from 'react';

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

Дивіться більше прикладів нижче.

Параметри

  • value: значення, яке ви хочете відкласти. Може мати будь-який тип.

Повернення

Під час початкового рендерингу повернуте відкладене значення буде таким самим, як ви вказали. Під час оновлень React спочатку спробує виконати повторний рендеринг зі старим значенням (таким чином, він поверне старе значення), а потім спробує виконати повторний рендеринг у фоновому режимі з новим значенням (таким чином, він поверне оновлене значення).

Застереження

  • Значення, які ви передаєте до useDeferredValue, мають бути або примітивними значеннями (наприклад, рядками та числами), або об'єктами, створеними поза рендерингом. Якщо ви створите новий об'єкт під час рендерингу і одразу передасте його до useDeferredValue, він буде різним на кожному рендері, що призведе до непотрібних фонових перерендерингів.

  • Коли useDeferredValue отримує інше значення (порівняно з Object.is), на додаток до поточного рендерингу (коли він все ще використовує попереднє значення), він планує повторний рендеринг у фоновому режимі з новим значенням. Фоновий рендеринг є переривчастим: якщо відбудеться чергове оновлення значення значення , React перезапустить фоновий рендеринг з нуля. Наприклад, якщо користувач вводить дані швидше, ніж графік, що отримує відкладене значення, може перерендерити їх, графік буде повторно рендерити тільки після того, як користувач припинить вводити дані.

  • useDeferredValue інтегровано з <Suspense>. Якщо оновлення фону, спричинене новим значенням, призупиняє роботу інтерфейсу, користувач не побачить запасного варіанту. Він побачить старе відкладене значення, доки дані не буде завантажено.

  • useDeferredValue саме по собі не запобігає зайвим мережевим запитам.

  • Не існує фіксованої затримки, спричиненої самим useDeferredValue. Як тільки React завершить основний рендеринг, React негайно почне працювати над фоновим рендерингом з новим відкладеним значенням. Будь-які оновлення, спричинені подіями (наприклад, введенням тексту), перериватимуть фоновий рендеринг і матимуть пріоритет над ним.

  • При перемальовуванні фону, спричиненому useDeferredValue, ефекти не застосовуються, доки його не буде зафіксовано на екрані. Якщо перемальовування фону призупинено, його ефекти буде запущено після завантаження даних та оновлення інтерфейсу користувача.


Використання

Показує застарілий вміст під час завантаження свіжого вмісту

Викличте useDeferredValue на верхньому рівні вашого компонента, щоб відкласти оновлення частини інтерфейсу користувача.

import { useState, useDeferredValue } from 'react';

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

Під час початкового рендеруинг відкладене значення CodeStep> буде таким самим, як і <CodeStep data-step="1">значення</CodeStep> який ви надали.</p> <p>Під час оновлень значення <CodeStep data-step="2">відкладеного значення</CodeStep> буде "відставати" від останнього <CodeStep data-step="1">значення</CodeStep> . Зокрема, React спочатку повторно відрендерить <em>без</em> оновлення відкладеного значення, а потім спробує повторно відрендерити з новим отриманим значенням у background.</p> <p><strong>Давайте розглянемо приклад, щоб побачити, коли це може бути корисним.</strong></p> <Примітка><p>Цей приклад припускає, що ви використовуєте джерело даних з підтримкою Suspense:</p> <ul> <li>Отримання даних за допомогою фреймворків з підтримкою Suspense, таких як <a href="https://relay.dev/docs/guided-tour/rendering/loading-states/" target="_blank" rel="nofollow noopener noreferrer">Relay</a> і <a href="https://nextjs. org/docs/getting-started/react-essentials" target="_blank" rel="nofollow noopener noreferrer">Next.js</a></li> <li>Лінивий код компонента з <a href="/reference/react/lazy"><code>lazy

  • Зчитування значення обіцянки за допомогою використання
  • Дізнайтеся більше про Suspense та його обмеження.

    У цьому прикладі компонент SearchResults призупиняє під час отримання результатів пошуку. Спробуйте ввести "a", дочекатися результатів, а потім відредагувати його на "ab". Результати для "a" буде замінено резервним варіантом завантаження.

    {
      "dependencies": {
        "react": "experimental",
        "react-dom": "experimental"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
      }
    }
    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>
        </>
      );
    }
    import { fetchData } from './data.js';
    
    // Note: this component is written using an experimental API
    // that's not yet available in stable versions of React.
    
    // For a realistic example you can follow today, try a framework
    // that's integrated with Suspense, like Relay or Next.js.
    
    export default function SearchResults({ query }) {
      if (query === '') {
        return null;
      }
      const albums = use(fetchData(`/search?q=${query}`));
      if (albums.length === 0) {
        return <p>No matches for <i>"{query}"</i></p>;
      }
      return (
        <ul>
          {albums.map(album => (
            <li key={album.id}>
              {album.title} ({album.year})
            </li>
          ))}
        </ul>
      );
    }
    
    // This is a workaround for a bug to get the demo running.
    // TODO: replace with real implementation when the bug is fixed.
    function use(promise) {
      if (promise.status === 'fulfilled') {
        return promise.value;
      } else if (promise.status === 'rejected') {
        throw promise.reason;
      } else if (promise.status === 'pending') {
        throw promise;
      } else {
        promise.status = 'pending';
        promise.then(
          result => {
            promise.status = 'fulfilled';
            promise.value = result;
          },
          reason => {
            promise.status = 'rejected';
            promise.reason = reason;
          },      
        );
        throw promise;
      }
    }
    // Note: the way you would do data fetching depends on
    // the framework that you use together with Suspense.
    // Normally, the caching logic would be inside a framework.
    
    let cache = new Map();
    
    export function fetchData(url) {
      if (!cache.has(url)) {
        cache.set(url, getData(url));
      }
      return cache.get(url);
    }
    
    async function getData(url) {
      if (url.startsWith('/search?q=')) {
        return await getSearchResults(url.slice('/search?q='.length));
      } else {
        throw Error('Not implemented');
      }
    }
    
    async function getSearchResults(query) {
      // Add a fake delay to make waiting noticeable.
      await new Promise(resolve => {
        setTimeout(resolve, 500);
      });
    
      const allAlbums = [{
        id: 13,
        title: 'Let It Be',
        year: 1970
      }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
      }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
      }, {
        id: 10,
        title: 'The Beatles',
        year: 1968
      }, {
        id: 9,
        title: 'Magical Mystery Tour',
        year: 1967
      }, {
        id: 8,
        title: 'Sgt. Pepper\'s Lonely Hearts Club Band',
        year: 1967
      }, {
        id: 7,
        title: 'Revolver',
        year: 1966
      }, {
        id: 6,
        title: 'Rubber Soul',
        year: 1965
      }, {
        id: 5,
        title: 'Help!',
        year: 1965
      }, {
        id: 4,
        title: 'Beatles For Sale',
        year: 1964
      }, {
        id: 3,
        title: 'A Hard Day\'s Night',
        year: 1964
      }, {
        id: 2,
        title: 'With The Beatles',
        year: 1963
      }, {
        id: 1,
        title: 'Please Please Me',
        year: 1963
      }];
    
      const lowerQuery = query.trim().toLowerCase();
      return allAlbums.filter(album => {
        const lowerTitle = album.title.toLowerCase();
        return (
          lowerTitle.startsWith(lowerQuery) ||
          lowerTitle.indexOf(' ' + lowerQuery) !== -1
        )
      });
    }
    input { margin: 10px; }

    Поширеним альтернативним шаблоном інтерфейсу є відкладення оновлення списку і продовження показу попередніх результатів, доки не будуть готові нові результати. Виклик 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>
        </>
      );
    }

    запит оновиться негайно, тому на вході буде показано нове значення. Однак deferredQuery зберігатиме попереднє значення, доки дані не буде завантажено, тому SearchResults показуватиме застарілі результати на деякий час.

    Введіть "a" у прикладі нижче, зачекайте, поки завантажаться результати, а потім відредагуйте введення на "ab". Зверніть увагу, що замість резервного варіанта Suspense ви побачите затемнений список застарілих результатів, доки не завантажаться нові результати:

    {
      "dependencies": {
        "react": "experimental",
        "react-dom": "experimental"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
      }
    }
    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>
        </>
      );
    }
    import { fetchData } from './data.js';
    
    // Note: this component is written using an experimental API
    // that's not yet available in stable versions of React.
    
    // For a realistic example you can follow today, try a framework
    // that's integrated with Suspense, like Relay or Next.js.
    
    export default function SearchResults({ query }) {
      if (query === '') {
        return null;
      }
      const albums = use(fetchData(`/search?q=${query}`));
      if (albums.length === 0) {
        return <p>No matches for <i>"{query}"</i></p>;
      }
      return (
        <ul>
          {albums.map(album => (
            <li key={album.id}>
              {album.title} ({album.year})
            </li>
          ))}
        </ul>
      );
    }
    
    // This is a workaround for a bug to get the demo running.
    // TODO: replace with real implementation when the bug is fixed.
    function use(promise) {
      if (promise.status === 'fulfilled') {
        return promise.value;
      } else if (promise.status === 'rejected') {
        throw promise.reason;
      } else if (promise.status === 'pending') {
        throw promise;
      } else {
        promise.status = 'pending';
        promise.then(
          result => {
            promise.status = 'fulfilled';
            promise.value = result;
          },
          reason => {
            promise.status = 'rejected';
            promise.reason = reason;
          },      
        );
        throw promise;
      }
    }
    // Note: the way you would do data fetching depends on
    // the framework that you use together with Suspense.
    // Normally, the caching logic would be inside a framework.
    
    let cache = new Map();
    
    export function fetchData(url) {
      if (!cache.has(url)) {
        cache.set(url, getData(url));
      }
      return cache.get(url);
    }
    
    async function getData(url) {
      if (url.startsWith('/search?q=')) {
        return await getSearchResults(url.slice('/search?q='.length));
      } else {
        throw Error('Not implemented');
      }
    }
    
    async function getSearchResults(query) {
      // Add a fake delay to make waiting noticeable.
      await new Promise(resolve => {
        setTimeout(resolve, 500);
      });
    
      const allAlbums = [{
        id: 13,
        title: 'Let It Be',
        year: 1970
      }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
      }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
      }, {
        id: 10,
        title: 'The Beatles',
        year: 1968
      }, {
        id: 9,
        title: 'Magical Mystery Tour',
        year: 1967
      }, {
        id: 8,
        title: 'Sgt. Pepper\'s Lonely Hearts Club Band',
        year: 1967
      }, {
        id: 7,
        title: 'Revolver',
        year: 1966
      }, {
        id: 6,
        title: 'Rubber Soul',
        year: 1965
      }, {
        id: 5,
        title: 'Help!',
        year: 1965
      }, {
        id: 4,
        title: 'Beatles For Sale',
        year: 1964
      }, {
        id: 3,
        title: 'A Hard Day\'s Night',
        year: 1964
      }, {
        id: 2,
        title: 'With The Beatles',
        year: 1963
      }, {
        id: 1,
        title: 'Please Please Me',
        year: 1963
      }];
    
      const lowerQuery = query.trim().toLowerCase();
      return allAlbums.filter(album => {
        const lowerTitle = album.title.toLowerCase();
        return (
          lowerTitle.startsWith(lowerQuery) ||
          lowerTitle.indexOf(' ' + lowerQuery) !== -1
        )
      });
    }
    input { margin: 10px; }

    Як працює відкладання значення під капотом?

    Ви можете уявити, що це відбувається у два кроки:

    1. Спочатку React повторно рендерить з новим запитом ("ab"), але зі старим deferredQuery (все ще "a"). Значення deferredQuery, яке ви передаєте до списку результатів, є відкладеним: воно "відстає" від значення запиту.

    2. У фоновому режимі React намагається перерендерити обидва запити та deferredQuery, оновлені до "ab". Якщо цей перерендеринг завершиться, React покаже це на екрані. Однак, якщо він призупиниться (результати для "ab" ще не завантажилися), React припинить цю спробу рендерингу і повторить її після того, як дані будуть завантажені. Користувач бачитиме застаріле відкладене значення, доки дані не будуть готові.

    Відкладений "фоновий" рендеринг переривається. Наприклад, якщо ви знову введете вхідні дані, React відкине їх і перезапуститься з новим значенням. React завжди буде використовувати останнє надане значення.

    Зауважте, що на кожне натискання клавіш все ще виконується мережевий запит. Тут відкладається відображення результатів (доки вони не будуть готові), а не самі мережеві запити. Навіть якщо користувач продовжує набирати текст, відповіді на кожне натискання клавіш кешуються, тому натискання клавіші Backspace відбувається миттєво і не призводить до повторного запиту.


    Вказує на те, що вміст застарілий

    У наведеному вище прикладі не вказано, що список результатів останнього запиту досі завантажується. Це може заплутати користувача, якщо нові результати завантажуються довго. Щоб користувачеві було зрозуміліше, що список результатів не відповідає останньому запиту, ви можете додати візуальну індикацію, коли відображається застарілий список результатів:

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

    З цією зміною, щойно ви почнете вводити текст, застарілий список результатів буде трохи затемнено, доки не завантажиться новий список результатів. Ви також можете додати CSS-перехід для затримки затемнення, щоб воно відбувалося поступово, як у наведеному нижче прикладі:

    {
      "dependencies": {
        "react": "experimental",
        "react-dom": "experimental"
      },
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test --env=jsdom",
        "eject": "react-scripts eject"
      }
    }
    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>
        </>
      );
    }
    import { fetchData } from './data.js';
    
    // Note: this component is written using an experimental API
    // that's not yet available in stable versions of React.
    
    // For a realistic example you can follow today, try a framework
    // that's integrated with Suspense, like Relay or Next.js.
    
    export default function SearchResults({ query }) {
      if (query === '') {
        return null;
      }
      const albums = use(fetchData(`/search?q=${query}`));
      if (albums.length === 0) {
        return <p>No matches for <i>"{query}"</i></p>;
      }
      return (
        <ul>
          {albums.map(album => (
            <li key={album.id}>
              {album.title} ({album.year})
            </li>
          ))}
        </ul>
      );
    }
    
    // This is a workaround for a bug to get the demo running.
    // TODO: replace with real implementation when the bug is fixed.
    function use(promise) {
      if (promise.status === 'fulfilled') {
        return promise.value;
      } else if (promise.status === 'rejected') {
        throw promise.reason;
      } else if (promise.status === 'pending') {
        throw promise;
      } else {
        promise.status = 'pending';
        promise.then(
          result => {
            promise.status = 'fulfilled';
            promise.value = result;
          },
          reason => {
            promise.status = 'rejected';
            promise.reason = reason;
          },      
        );
        throw promise;
      }
    }
    // Note: the way you would do data fetching depends on
    // the framework that you use together with Suspense.
    // Normally, the caching logic would be inside a framework.
    
    let cache = new Map();
    
    export function fetchData(url) {
      if (!cache.has(url)) {
        cache.set(url, getData(url));
      }
      return cache.get(url);
    }
    
    async function getData(url) {
      if (url.startsWith('/search?q=')) {
        return await getSearchResults(url.slice('/search?q='.length));
      } else {
        throw Error('Not implemented');
      }
    }
    
    async function getSearchResults(query) {
      // Add a fake delay to make waiting noticeable.
      await new Promise(resolve => {
        setTimeout(resolve, 500);
      });
    
      const allAlbums = [{
        id: 13,
        title: 'Let It Be',
        year: 1970
      }, {
        id: 12,
        title: 'Abbey Road',
        year: 1969
      }, {
        id: 11,
        title: 'Yellow Submarine',
        year: 1969
      }, {
        id: 10,
        title: 'The Beatles',
        year: 1968
      }, {
        id: 9,
        title: 'Magical Mystery Tour',
        year: 1967
      }, {
        id: 8,
        title: 'Sgt. Pepper\'s Lonely Hearts Club Band',
        year: 1967
      }, {
        id: 7,
        title: 'Revolver',
        year: 1966
      }, {
        id: 6,
        title: 'Rubber Soul',
        year: 1965
      }, {
        id: 5,
        title: 'Help!',
        year: 1965
      }, {
        id: 4,
        title: 'Beatles For Sale',
        year: 1964
      }, {
        id: 3,
        title: 'A Hard Day\'s Night',
        year: 1964
      }, {
        id: 2,
        title: 'With The Beatles',
        year: 1963
      }, {
        id: 1,
        title: 'Please Please Me',
        year: 1963
      }];
    
      const lowerQuery = query.trim().toLowerCase();
      return allAlbums.filter(album => {
        const lowerTitle = album.title.toLowerCase();
        return (
          lowerTitle.startsWith(lowerQuery) ||
          lowerTitle.indexOf(' ' + lowerQuery) !== -1
        )
      });
    }
    input { margin: 10px; }

    Відкладення повторного рендерингу для частини інтерфейсу

    Ви також можете застосувати useDeferredValue для оптимізації продуктивності. Це корисно, коли частина вашого інтерфейсу повільно перемальовується, немає простого способу оптимізувати її, і ви не хочете, щоб вона блокувала решту інтерфейсу.

    Уявіть, що у вас є текстове поле та компонент (наприклад, діаграма або довгий список), який повторно рендериться після кожного натискання клавіші:

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

    По-перше, оптимізуйте SlowList, щоб він не перемальовувався, коли його пропси залишаються незмінними. Для цього обгорніть його у memo:

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

    Втім, це допоможе лише у випадку, якщо пропси SlowList є такими самими , як і під час попереднього рендеру. Проблема, з якою ви зіткнулися зараз, полягає у тому, що він працює повільно, коли вони відрізняються, і коли вам насправді потрібно показати інший візуальний вивід.

    Основна проблема продуктивності полягає у тому, що кожного разу, коли ви вводите дані, SlowList отримує нові пропси, і повторний рендеринг усього його дерева призводить до відчуття ривків. У цьому випадку 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 намагатиметься оновити список якнайшвидше, але не блокуватиме користувачеві введення.

    Відкладений перевідображення списку

    У цьому прикладі кожен елемент компонента 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} />
        </>
      );
    }
    import { memo } from 'react';
    
    const SlowList = memo(function SlowList({ text }) {
      // Log once. The actual slowdown is inside SlowItem.
      console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');
    
      let items = [];
      for (let i = 0; i < 250; i++) {
        items.push(<SlowItem key={i} text={text} />);
      }
      return (
        <ul className="items">
          {items}
        </ul>
      );
    });
    
    function SlowItem({ text }) {
      let startTime = performance.now();
      while (performance.now() - startTime < 1) {
        // Do nothing for 1 ms per item to emulate extremely slow code
      }
    
      return (
        <li className="item">
          Text: {text}
        </li>
      )
    }
    
    export default SlowList;
    .items {
      padding: 0;
    }
    
    .item {
      list-style: none;
      display: block;
      height: 40px;
      padding: 5px;
      margin-top: 10px;
      border-radius: 4px;
      border: 1px solid #aaa;
    }

    Неоптимізоване повторне відображення списку

    У цьому прикладі кожен елемент компонента SlowList штучно сповільнено , але немає useDeferredValue.

    Зверніть увагу на те, що введення відчувається дуже ривками. Це тому, що без useDeferredValue кожне натискання клавіші змушує весь список негайно перемальовуватися у неперервний спосіб.

    import { useState } from 'react';
    import SlowList from './SlowList.js';
    
    export default function App() {
      const [text, setText] = useState('');
      return (
        <>
          <input value={text} onChange={e => setText(e.target.value)} />
          <SlowList text={text} />
        </>
      );
    }
    import { memo } from 'react';
    
    const SlowList = memo(function SlowList({ text }) {
      // Log once. The actual slowdown is inside SlowItem.
      console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');
    
      let items = [];
      for (let i = 0; i < 250; i++) {
        items.push(<SlowItem key={i} text={text} />);
      }
      return (
        <ul className="items">
          {items}
        </ul>
      );
    });
    
    function SlowItem({ text }) {
      let startTime = performance.now();
      while (performance.now() - startTime < 1) {
        // Do nothing for 1 ms per item to emulate extremely slow code
      }
    
      return (
        <li className="item">
          Text: {text}
        </li>
      )
    }
    
    export default SlowList;
    .items {
      padding: 0;
    }
    
    .item {
      list-style: none;
      display: block;
      height: 40px;
      padding: 5px;
      margin-top: 10px;
      border-radius: 4px;
      border: 1px solid #aaa;
    }

    Ця оптимізація вимагає, щоб SlowList було обгорнуто у memo. Це пов'язано з тим, що кожного разу, коли змінюється text, React повинен мати можливість швидко повторно відрендерити батьківський компонент. Під час цього повторного рендерингу deferredText все ще має попереднє значення, тому SlowList може пропустити перерендеринг (його пропси не змінилися). Без memo, йому все одно довелося б перерендерити, що суперечить сенсу оптимізації.

    Як відкладання значення відрізняється від дебаунсингу та дроселювання?

    Існує два поширені методи оптимізації, які ви могли використовувати раніше у цьому сценарії:

    • Затримка означає, що ви чекатимете, доки користувач припинить друкувати (наприклад, на секунду) перед оновленням списку.
    • Трослінг означає, що ви будете оновлювати список час від часу (наприклад, максимум раз на секунду).

    Хоча ці методи корисні в деяких випадках, useDeferredValue краще підходить для оптимізації рендерингу, оскільки він глибоко інтегрований з самим React і адаптується до пристрою користувача.

    На відміну від дебаунсингу або дроселювання, цей метод не вимагає вибору фіксованої затримки. Якщо пристрій користувача швидкий (наприклад, потужний ноутбук), відкладений рендеринг відбудеться майже миттєво і не буде помітним. Якщо пристрій користувача повільний, список буде "відставати" від введення пропорційно до того, наскільки повільним є пристрій.

    Також, на відміну від дебаунсингу або дроселювання, відкладений рендеринг, що виконується useDeferredValue, переривається за замовчуванням. Це означає, що якщо React перебуває в процесі рендерингу великого списку, але користувач натискає ще одну клавішу, React перерве рендеринг, обробить натискання клавіші, а потім знову почне рендеринг у фоновому режимі. На противагу цьому, дебаунсинг та дроселювання все ще створюють незручний досвід, оскільки вони блокують: вони просто відкладають момент, коли рендеринг блокує натискання клавіші.

    Якщо робота, яку ви оптимізуєте, не відбувається під час рендерингу, дебаунсинг і дроселювання все одно корисні. Наприклад, вони дозволяють зменшити кількість мережевих запитів. Ви також можете використовувати ці методи разом.