useTransition

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

const [isPending, startTransition] = useTransition()

Справочник

useTransition()

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

import { useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

Больше примеров ниже.

Параметры

useTransition не принимает никаких параметров.

Возвращаемое значение

useTransition возвращает массив ровно из двух элементов:

  1. Флаг isPending, который указывает, есть ли ожидающий Переход.
  2. Функция startTransition, которая позволяет помечать изменения как Переход.

startTransition(action)

Функция startTransition, которую возвращает useTransition, позволяет помечать изменения как Переход.

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

Note

Функции, вызываемые в startTransition называются «Действия»(“Actions”).

Функция, переданная startTransitionназывается «Действие» (“Action”). По соглашению, любой колбэк, вызываемый внутри startTransition (например, колбэк-проп), должен называться action или иметь суффикс “Action” в имени:

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
submitAction();
});
}}
>
Отправить
</button>
);
}

Параметры

  • action: Функция, которая обновляет некоторое состояние, вызывая одну или несколько функций set. React немедленно вызывает action без параметров и помечает все обновления состояния, которые были синхронно запланированы во время вызова функции action, как Переходы. Любые асинхронные вызовы, которые ожидаются в action, будут включены в Переход, но в настоящее время требуют обёртывания любых функций set после await в дополнительный startTransition (см. Устранение неполадок). Обновления состояния, помеченные как Переходы, будут неблокирующими и не будут отображать нежелательные индикаторы загрузки.

Возвращаемое значение

startTransition ничего не возвращает.

Подводные камни

  • useTransition — это хук, поэтому его можно вызывать только внутри компонентов или пользовательских хуков. Если вам нужно запустить Переход где-то ещё (например, из библиотеки данных), вместо этого вызовите автономный startTransition.

  • Вы можете обернуть обновление в Переход, только если у вас есть доступ к функции set этого состояния. Если вы хотите запустить Переход в ответ на какой-либо проп или значение пользовательского хука, попробуйте вместо этого useDeferredValue.

  • Функция, которую вы передаёте в startTransition, вызывается немедленно, помечая все обновления состояния, которые происходят во время его выполнения, как Переходы. Если вы попытаетесь обновить состояния в, например, setTimeout, то они не будут помечены как Переходы.

  • Вы должны обернуть любые обновления состояния после асинхронных запросов в дополнительный startTransition, чтобы пометить их как Переходы. Это известное ограничение, которое мы планируем исправить в будущем (см. Устранение неполадок).

  • Функция startTransition имеет стабильную идентичность, поэтому вы часто увидите, что её опускают в зависимостях Эффектов, но добавление её в список зависимостей не вызовет повторного срабатывания Эффекта. Если линтер позволяет опустить зависимость без ошибок, то это можно смело делать. Узнайте больше о том, как удалять зависимости Эффекта.

  • Обновление состояния, помеченное как Переход, будет прервано другими обновлениями состояния. Например, если вы обновляете компонент диаграммы внутри Перехода, а затем начинаете вводить текст в поле ввода, когда диаграмма находится в середине повторного рендера, React перезапустит работу по рендерингу компонента диаграммы после обработки обновления поля ввода.

  • Обновления Перехода не могут быть использованы для управления текстовыми полями ввода.

  • Если существует несколько активных Переходов, React, в настоящее время, группирует их вместе. Это ограничение, вероятно, будет убрано в будущих версиях.

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

Выполняйте неблокирующие изменения с помощью Действий.

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

import {useState, useTransition} from 'react';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
// ...
}

useTransition возвращает массив ровно из двух элементов:

  1. Флаг isPending указывает, есть ли ожидающий Переход.
  2. Функция startTransition позволяет создать Действие.

Чтобы начать Переход, передайте функцию в startTransition следующим образом:

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);

function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}

Функция, передаваемая в startTransition, называется «Действие». Внутри Действия можно обновлять состояние и (опционально) выполнять побочные эффекты. Эта работа будет выполнена в фоновом режиме, не блокируя взаимодействие пользователя со страницей. Переход может включать несколько Действий, и пока Переход выполняется, ваш интерфейс остаётся отзывчивым. Например, если пользователь нажмёт на вкладку, а затем передумает и кликнет на другую — второй клик будет обработан немедленно, не дожидаясь завершения первого обновления.

Чтобы дать пользователю обратную связь о выполняющихся Переходах, состояние isPending переключается в true при первом вызове startTransition и остаётся true, пока все Действия не завершатся и финальное состояние не будет отображено пользователю. Переходы гарантируют, что побочные эффекты в Действиях выполняются последовательно, чтобы избежать нежелательных индикаторов загрузки. Также, во время выполнения Перехода можно обеспечить немедленную обратную связь с помощью useOptimistic.

Разница между Действиями и обычной обработкой событий

Example 1 of 2:
Обновление количества в Действии

В этом примере функция updateQuantity имитирует запрос к серверу для обновления количества товара в корзине. Эта функция искусственно замедлена, чтобы выполнение запроса занимало как минимум одну секунду.

Попробуйте быстро несколько раз обновить количество. Обратите внимание, что состояние “Total” остаётся в ожидании, пока выполняются запросы, и обновляется только после завершения последнего запроса. Поскольку обновление выполняется внутри Действия, значение “quantity” можно продолжать изменять, даже пока запрос ещё выполняется.

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();

  const updateQuantityAction = async newQuantity => {
    // Чтобы получить доступ к состоянию ожидания перехода,
    // вызовите startTransition ещё раз.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Оформление заказа</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total quantity={quantity} isPending={isPending} />
    </div>
  );
}

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

Для распространённых случаев использования React предоставляет встроенные абстракции, такие как:

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


Передача пропа action из компонентов

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

Например, компонент TabButton оборачивает свою логику onClick в проп action:

export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
action();
});
}}>
{children}
</button>
);
}

Поскольку родительский компонент обновляет своё состояние внутри action, это обновление состояния помечается как Переход. Это значит что, вы можете нажать на «Публикации», а затем сразу же нажать на «Контакты» — и это не блокирует взаимодействие с пользователем:

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


Отображение ожидающего визуального состояния

Вы можете использовать булево значение isPending, возвращаемое useTransition, чтобы указать пользователю, что происходит Переход. Например, кнопка вкладки может иметь специальное визуальное состояние «ожидание»:

function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...

Обратите внимание, что нажатие на «Публикации» теперь кажется более отзывчивым, потому что кнопка вкладки сразу же обновляется:

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


Предотвращение нежелательных индикаторов загрузки

В этом примере компонент PostsTab получает некоторые данные, используя use. Когда вы нажимаете на вкладку «Публикации», компонент PostsTab задерживается, что приводит к появлению ближайшего запасного варианта загрузки:

import { Suspense, useState } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>🌀 Загрузка...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        action={() => setTab('about')}
      >
        Обо мне
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        action={() => setTab('posts')}
      >
        Публикации
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        action={() => setTab('contact')}
      >
        Контакты
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

Скрытие всего контейнера вкладок для отображения индикатора загрузки приводит к неприятному пользовательскому опыту. Если вы добавите useTransition в TabButton, вы можете вместо этого показать состояние ожидания в кнопке вкладки.

Обратите внимание, что нажатие на «Публикации» больше не заменяет весь контейнер вкладок на спиннер:

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}

Узнайте больше об использовании Переходов с Задержкой.

Note

Переходы будут «ждать» достаточно долго, чтобы не скрыть уже показанный контент (например, контейнер вкладок). Если бы во вкладке «Публикации» присутствовала вложенная граница <Suspense>, Переход бы её не «ждал».


Создание маршрутизатора, поддерживающего Задержку

Если вы создаёте React-фреймворк или маршрутизатор, мы рекомендуем помечать навигацию между страницами как Переходы.

function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

Это рекомендуется по трём причинам:

Вот небольшой упрощённый пример маршрутизатора, использующего Переходы для навигации.

import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Загрузка...</h2>;
}

Note

Ожидается, что маршрутизаторы, поддерживающие Задержку, по умолчанию оборачивают обновления навигации в Переходы.


Отображение ошибки пользователю с помощью границы ошибок

Если функция, переданная в startTransition, выбрасывает ошибку, вы можете отобразить её пользователю с помощью границы ошибок. Чтобы использовать границу ошибок, оберните компонент, в котором вызывается useTransition, в границу ошибок. Как только функция, переданная в startTransition, выдаст ошибку, будет отображён запасной интерфейс от границы ошибок.

import { useTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️Что-то пошло не так</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // Для демонстрации работы границы ошибок
  if (comment == null) {
    throw new Error("Пример ошибки: Искусственно выброшенная ошибка для проверки границы ошибок");
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Специально не передаём комментарий
          // чтобы сгенерировать ошибку
          addComment();
        });
      }}
    >
      Добавить комментарий
    </button>
  );
}


Устранение неполадок

Обновление ввода во время Перехода не работает

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

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Нельзя использовать Переходы для контролируемого состояния ввода
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

Это происходит, потому что Переходы являются неблокирующими, но обновление ввода в ответ на событие изменения должно происходить синхронно. Если вы хотите запустить Переход при вводе текста, у вас есть два варианта:

  1. Вы можете объявить две отдельные переменные состояния: одну для состояния ввода (которая всегда обновляется синхронно), и одну, которую вы будете обновлять во время Перехода. Это позволит вам управлять вводом с использованием синхронного состояния и передавать переменную состояния Перехода (которая будет «отставать» от ввода) в остальную логику рендеринга.
  2. В качестве альтернативы, вы можете использовать одну переменную состояния и добавить useDeferredValue, так что она будет «отставать» от реального значения. Она будет вызывать неблокирующие перерисовки, чтобы «догнать» новое значение автоматически.

React не обрабатывает моё обновление состояния как Переход

Когда вы оборачиваете обновление состояния в Переход, убедитесь, что оно происходит во время вызова startTransition.

startTransition(() => {
// ✅ Установка состояния *во время* вызова startTransition
setPage('/about');
});

Функция, которую вы передаёте startTransition, должна быть синхронной. Вы не можете отметить обновление как Переход вот так:

startTransition(() => {
// ❌ Установка состояния *после* вызова startTransition
setTimeout(() => {
setPage('/about');
}, 1000);
});

Вместо этого вы можете сделать следующее:

setTimeout(() => {
startTransition(() => {
// ✅ Установка состояния *во время* вызова startTransition
setPage('/about');
});
}, 1000);

React не считает обновление состояния после await Переходом

Когда вы используете await внутри функции startTransition, обновления состояния, которые происходят после await, не помечаются как Переходы. Чтобы исправить это, необходимо обернуть каждое обновление состояния после await в отдельный вызов startTransition:

startTransition(async () => {
await someAsyncFunction();
// ❌ Не используется startTransition после await
setPage('/about');
});

Однако, это будет работать вместо этого:

startTransition(async () => {
await someAsyncFunction();
// ✅ Использование startTransition *после* await
startTransition(() => {
setPage('/about');
});
});

Это ограничение JavaScript, связанное с тем, что React теряет область видимости асинхронного контекста. В будущем, когда станет доступен AsyncContext, это ограничение будет снято.


Я хочу вызвать useTransition вне компонента

Вы не можете вызывать useTransition вне компонента, так как это хук. В этом случае, используйте отдельный метод startTransition. Он работает так же, но не предоставляет индикатор isPending.


Функция, которую я передаю startTransition, сразу же выполняется

Если вы запустите этот код, он напечатает 1, 2, 3:

console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);

Ожидается, что будет напечатано 1, 2, 3. Функция, которую вы передаёте startTransition, не задерживается. В отличие от setTimeout в браузере, она не запускает колбэк позже. React немедленно выполняет вашу функцию, но любые обновления состояния, запланированные во время её выполнения, помечаются как Переходы. Можно представить, что это работает так:

// Упрощённая версия того, как работает React

let isInsideTransition = false;

function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}

function setState() {
if (isInsideTransition) {
// ... запланировать обновление состояния Перехода ...
} else {
// ... запланировать срочное обновление состояния ...
}
}

Мои обновления состояния в Переходах приходят не по порядку

Если вы используете await внутри startTransition, вы можете столкнуться с тем, что обновления будут происходить не в том порядке.

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

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

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  // Храним фактическое количество в отдельном состоянии, чтобы показать расхождение.
  const [clientQuantity, setClientQuantity] = useState(1);
  
  const updateQuantityAction = newQuantity => {
    setClientQuantity(newQuantity);

    // Получаем доступ к состоянию ожидания перехода,
    // обернув вызов снова в startTransition.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Оформление заказа</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}

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

Это ожидаемое поведение, так как Действия внутри одного Перехода не гарантируют порядок выполнения. Для распространённых случаев React предоставляет более высокоуровневые абстракции, такие как useActionState и <form> действия, которые сами управляют порядком. Для более сложных кейсов придётся реализовать собственную логику очередей и отмены запросов.