Перейти к содержанию

useDeferredValue

useDeferredValue - это хук React, который позволяет отложить обновление части пользовательского интерфейса.

1
const deferredValue = useDeferredValue(value);

Описание

useDeferredValue(value)

Вызовите useDeferredValue на верхнем уровне вашего компонента, чтобы получить отложенную версию этого значения.

1
2
3
4
5
6
7
import { useState, useDeferredValue } from 'react';

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

Параметры

  • value: Значение, которое вы хотите отложить. Оно может иметь любой тип.

Возвращает

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

Предупреждения

  • Значения, которые вы передаете в useDeferredValue, должны быть либо примитивными значениями (такими как строки и числа), либо объектами, созданными вне рендеринга. Если вы создадите новый объект во время рендеринга и сразу передадите его в useDeferredValue, он будет отличаться при каждом рендеринге, что приведет к ненужным повторным рендерам фона.
  • Когда useDeferredValue получает другое значение (по сравнению с Object.is), в дополнение к текущему рендеру (когда он все еще использует предыдущее значение), он планирует повторный рендер в фоновом режиме с новым значением. Фоновый рендеринг можно прервать: если произойдет очередное обновление value, React перезапустит фоновый рендеринг с нуля. Например, если пользователь набирает текст в поле ввода быстрее, чем график, получающий отложенное значение, успевает отрисоваться, график отрисуется только после того, как пользователь перестанет набирать текст.
  • useDeferredValue интегрировано с <Suspense>. Если фоновое обновление, вызванное новым значением, приостанавливает работу пользовательского интерфейса, пользователь не увидит откат. Он будет видеть старое отложенное значение до тех пор, пока данные не загрузятся.
  • Функция useDeferredValue сама по себе не предотвращает дополнительные сетевые запросы.
  • Не существует фиксированной задержки, вызванной самим useDeferredValue. Как только React завершит первоначальный рендеринг, React немедленно начнет работу над фоновым рендерингом с новым отложенным значением. Любые обновления, вызванные событиями (например, вводом текста), будут прерывать фоновый рендеринг и получат приоритет над ним.
  • Фоновый рендеринг, вызванный useDeferredValue, не запускает эффекты до тех пор, пока они не будут зафиксированы на экране. Если фоновый рендеринг приостановлен, его Эффекты будут запущены после загрузки данных и обновления пользовательского интерфейса.

Использование

Показ устаревшего содержимого во время загрузки свежего

Вызовите useDeferredValue на верхнем уровне вашего компонента, чтобы отложить обновление некоторой части вашего пользовательского интерфейса.

1
2
3
4
5
6
7
import { useState, useDeferredValue } from 'react';

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

Во время первоначального рендеринга отложенное значение будет таким же, как и предоставленное вами значение.

Во время обновлений отложенное значение будет "отставать" от последнего значения. В частности, React сначала выполнит рендеринг без обновления отложенного значения, а затем попытается выполнить рендеринг с вновь полученным значением в фоновом режиме.

Давайте рассмотрим пример, чтобы увидеть, когда это полезно.

Пример

Этот пример предполагает, что вы используете один из источников данных с поддержкой Suspense:

  • Получение данных с помощью фреймворков с поддержкой Suspense, таких как Relay и Next.js.
  • Ленивая загрузка кода компонента с помощью lazy

Узнайте больше о Suspense и его ограничениях

В этом примере компонент SearchResults приостанавливается на время получения результатов поиска. Попробуйте набрать "a", дождаться результатов, а затем изменить его на "ab". Результаты для "a" будут заменены загружаемым возвратом.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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>
        </>
    );
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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". Обратите внимание, что вместо отката к приостановке теперь отображается список несвежих результатов, пока не загрузятся новые:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 пытается выполнить повторный рендеринг с обновленными значениями query и deferredQuery до "ab". Если этот повторный рендеринг завершится, React покажет его на экране. Однако, если он приостановится (результаты для "ab" еще не загружены), React оставит эту попытку рендеринга и повторит ее снова после загрузки данных. Пользователь будет видеть отложенное значение до тех пор, пока данные не будут готовы.

Отложенный "фоновый" рендеринг можно прервать. Например, если вы снова введете данные в поле ввода, React покинет его и перезапустит с новым значением. React всегда будет использовать последнее предоставленное значение.

Обратите внимание, что при каждом нажатии клавиши все равно происходит запрос к сети. Здесь откладывается отображение результатов (пока они не будут готовы), а не сами сетевые запросы. Даже если пользователь продолжает набирать текст, ответы на каждое нажатие клавиши кэшируются, поэтому нажатие Backspace происходит мгновенно и не требует повторной выборки.

Указывает на то, что содержимое устарело

В приведенном выше примере нет указания на то, что список результатов последнего запроса все еще загружается. Это может сбить пользователя с толку, если новые результаты загружаются долго. Чтобы сделать более очевидным для пользователя, что список результатов не соответствует последнему запросу, можно добавить визуальную индикацию, когда отображается несвежий список результатов:

1
2
3
4
5
6
7
<div
    style={{
        opacity: query !== deferredQuery ? 0.5 : 1,
    }}
>
    <SearchResults query={deferredQuery} />
</div>

При таком изменении, как только вы начинаете вводить текст, список неактуальных результатов слегка затемняется, пока не загрузится новый список результатов. Вы также можете добавить CSS-переход для задержки затемнения, чтобы оно было постепенным, как в примере ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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>
        </>
    );
}

Откладывание повторного рендеринга для части пользовательского интерфейса

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

Представьте, что у вас есть текстовое поле и компонент (например, график или длинный список), который перерисовывается при каждом нажатии клавиши:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function App() {
    const [text, setText] = useState('');
    return (
        <>
            <input
                value={text}
                onChange={(e) => setText(e.target.value)}
            />
            <SlowList text={text} />
        </>
    );
}

Во-первых, оптимизируйте SlowList для пропуска повторного рендеринга, когда его пропсы одинаковы. Для этого оберните это в memo:

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

Однако это помогает только в том случае, если пропс SlowList такой же, как и во время предыдущего рендеринга. Проблема, с которой вы столкнулись сейчас, заключается в том, что это медленно, когда они разные, и когда вам действительно нужно показать разные визуальные результаты.

Конкретно, основная проблема производительности заключается в том, что всякий раз, когда вы вводите данные на вход, SlowList получает новые пропсы, и повторное отображение всего дерева приводит к тому, что ввод становится неаккуратным. В этом случае useDeferredValue позволяет вам установить приоритет обновления ввода (которое должно быть быстрым) над обновлением списка результатов (которое может быть более медленным):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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. Отложенное повторное отображение списка

В этом примере каждый элемент компонента SlowList искусственно замедляется, чтобы вы могли увидеть, как useDeferredValue позволяет вам сохранить отзывчивость ввода. Наберите текст на клавиатуре и обратите внимание, что текст набирается быстро, в то время как список "отстает" от него.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;

2. Неоптимизированный повторный рендеринг списка

В этом примере каждый элемент в компоненте SlowList искусственно замедлен, но нет useDeferredValue.

Обратите внимание на то, что ввод в поле ввода выглядит очень неаккуратно. Это потому, что без useDeferredValue каждое нажатие клавиши заставляет весь список немедленно перерисовываться, не прерываясь.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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} />
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;

Замечание

Эта оптимизация требует, чтобы SlowList был обернут в memo. Это происходит потому, что всякий раз, когда text изменяется, React должен иметь возможность быстро перерисовать родительский компонент. Во время этой перерисовки deferredText все еще имеет свое предыдущее значение, поэтому SlowList может пропустить перерисовку (его пропсы не изменились). Без memo, ему все равно пришлось бы перерисовываться, что сводит на нет смысл оптимизации.

Чем отсрочка значения отличается от дебаширования и дросселирования?

Есть две распространенные техники оптимизации, которые вы могли использовать раньше в этом сценарии:

  • Дебаунсинг означает, что вы будете ждать, пока пользователь прекратит печатать (например, на секунду), прежде чем обновить список.
  • Дросселирование означает, что вы будете обновлять список время от времени (например, не чаще одного раза в секунду).

Хотя эти методы полезны в некоторых случаях, useDeferredValue лучше подходит для оптимизации рендеринга, поскольку он глубоко интегрирован в сам React и адаптируется к устройству пользователя.

В отличие от дебаунсинга или дросселирования, он не требует выбора какой-либо фиксированной задержки. Если устройство пользователя быстрое (например, мощный ноутбук), отложенный повторный рендеринг произойдет почти мгновенно и не будет заметен. Если устройство пользователя медленное, список будет "отставать" от ввода пропорционально скорости устройства.

Также, в отличие от дебаунсинга или дросселирования, отложенный рендеринг, выполняемый useDeferredValue, по умолчанию прерывается. Это означает, что если React находится в середине рендеринга большого списка, но пользователь нажимает другую клавишу, React прервет этот рендеринг, обработает нажатие клавиши, а затем снова начнет рендеринг в фоновом режиме. В отличие от этого, дебаунсинг и дросселирование все еще вызывают неприятные ощущения, потому что они блокируют: они просто откладывают момент, когда рендеринг блокирует нажатие клавиши.

Если работа, которую вы оптимизируете, не происходит во время рендеринга, дебаунсинг и дросселирование все равно полезны. Например, они могут позволить вам выполнять меньше сетевых запросов. Вы также можете использовать эти методы вместе.

Источник — https://react.dev/reference/react/useDeferredValue

Комментарии