useTransition

useTransition — это React хук, который позволяет вам обновлять состояние, не блокируя UI.

const [isPending, startTransition] = useTransition()

Справочник

useTransition()

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

import { useTransition } from 'react';

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

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

Параметры

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

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

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

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

Функция startTransition

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

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

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

Параметры

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

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

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

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

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

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

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

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

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


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

Пометка обновления состояния как неблокирующего перехода

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

import { useState, useTransition } from 'react';

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

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

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

Затем вы можете пометить обновление состояния как переход следующим образом:

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

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

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

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

Разница между useTransition и обычными обновлениями состояния

Example 1 of 2:
Обновление текущей вкладки в переходе

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

Нажмите на “Posts”, а затем сразу нажмите на “Contact”. Обратите внимание, что это прерывает медленный рендеринг “Posts”. Вкладка “Contact” отображается сразу. Поскольку это обновление состояния помечено как переход, медленный повторный рендеринг не блокирует пользовательский интерфейс.

import { useState, useTransition } 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 [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

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

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}


Updating the parent component in a transition

Вы также можете обновить состояние родительского компонента с помощью вызова useTransition. Например, этот компонент TabButton заключает свою логику onClick в переход:

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

Поскольку родительский компонент обновляет своё состояние внутри обработчика события onClick, это обновление состояния помечается как переход. Вот поэтому, как и в предыдущем примере, вы можете нажать на “Posts”, а затем сразу же нажать на “Contact”. Обновление выбранной вкладки помечается как переход, поэтому взаимодействия пользователя не блокируются.

import { useTransition } from 'react';

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


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

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

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

Обратите внимание, что нажатие на “Posts” теперь кажется более отзывчивым, потому что кнопка вкладки сразу же обновляется:

import { useTransition } from 'react';

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


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

В этом примере компонент PostsTab получает некоторые данные, используя источник данных поддерживающий Задержку. Когда вы нажимаете на вкладку “Posts”, компонент 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>🌀 Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

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

Обратите внимание, что нажатие на “Posts” больше не заменяет весь контейнер вкладок на спиннер:

import { useTransition } from 'react';

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

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

Note

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


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

If you’re building a React framework or a router, we recommend marking page navigations as transitions. Если вы создаёте 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>🌀 Loading...</h2>;
}

Note

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


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

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

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

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);

Аналогично, вы не можете отметить обновление как переход вот так:

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

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

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

Я хочу вызвать 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 {
// ... запланировать срочное обновление состояния ...
}
}