Что нового в React 18 — useTransition, useDeferredValue, useSyncExternalStore, useInsertionEffect и useId
Important: все примеры предполагают React 18+ и соответствующую сборку. При миграции проверяйте совместимость библиотек и тестируйте на реальных сценариях.

В марте 2022 года команда React официально выпустила React 18. В центре релиза — концепция «конкурентного рендеринга» (concurrent rendering), идея которой — сделать процесс обновления DOM прерываемым и управляемым по приоритетам. Это позволяет улучшать отзывчивость интерфейса в сложных приложениях.
Ниже мы разберём основные новые хуки: useTransition, useDeferredValue, useSyncExternalStore, useInsertionEffect и useId — как они работают, где помогают, где не помогают, и как безопасно внедрять их в проект.
Коротко о concurrent rendering
Concurrent rendering — режим, в котором React может приостанавливать и возобновлять работу по рендерингу, чтобы отдавать приоритет срочным обновлениям (например, вводу пользователя) перед менее срочными (например, тяжёлыми вычислениями списка). Это не всегда означает асинхронный код в вашем приложении — это модель приоритизации внутри React.
Понятие: concurrent rendering — модель рендеринга, позволяющая React прерывать и возобновлять обновления по приоритетам.
useTransition — понижение приоритета обновления
useTransition позволяет пометить обновления состояния как «несрочные», чтобы срочные обновления могли прервать их. Типичный сценарий — ввод в поле поиска (срочно) и одновременное обновление тяжёлого списка результатов (несрочно).
Пример компонента SearchPage, имитирующий поиск и обновляющий два состояния — поле ввода и большой список результатов:
import { useState } from "react";
function SearchPage() {
const [input, setInput] = useState("")
const [list, setList] = useState([]);
const listSize = 30000
function handleChange(e) {
setInput(e.target.value);
const listItems = [];
for (let i = 0; i < listSize; i++){
listItems.push(e.target.value);
}
setList(listItems);
}
return (
{list.map((item, index) => {
return {item}
})}
);
}
export default SearchPage;Если вводить символы быстро, следует заметить задержку: обновление ввода и списка конкурируют за ресурсы, из-за чего ввод может «лагать».
Обновим компонент с использованием useTransition:
import {useState, useTransition} from "react";
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [input, setInput] = useState("")
const [list, setList] = useState([]);
const listSize = 30000
function handleChange(e) {
setInput(e.target.value);
startTransition(() => {
const listItems = [];
for (let i = 0; i < listSize; i++){
listItems.push(e.target.value);
}
setList(listItems);
});
}
return (
{isPending ? "...Loading results" : list.map((item, index) => {
return {item}
})}
);
}
export default SearchPage;Что меняется: ввод становится немедленным, а обновление списка выполняется с пониженным приоритетом. Флаг isPending указывает на текущую «переходную» операцию.
Совет: используйте startTransition вокруг тяжёлых вычислений или setState, которые могут быть отложены, чтобы улучшить отзывчивость UI.
useDeferredValue — удержание старого значения во время обновления
useDeferredValue позволяет компоненту временно «останавливать» значение и показывать старое значение, пока новый рендер не завершится. Это удобно, когда необязательно сразу показывать изменения в тяжёлом списке, но важно, чтобы ввод пользователя был отзывчивым.
Пример с использованием useDeferredValue:
import { useState, useTransition, useDeferredValue } from "react";
function SearchPage() {
const [,startTransition] = useTransition();
const [input, setInput] = useState("")
const [list, setList] = useState([]);
const listSize = 30000
function handleChange(e) {
setInput(e.target.value);
startTransition(() => {
const listItems = [];
for (let i = 0; i < listSize; i++){
listItems.push(e.target.value);
}
setList(listItems);
});
}
const deferredValue = useDeferredValue(input);
return (
{list.map((item, index) => {
return {item}
})}
);
}
export default SearchPage;Отличие от useTransition: useDeferredValue возвращает «отложенное» значение (deferredValue), которое остаётся прежним до завершения перехода, а интерфейс может продолжать обрабатывать ввод.
useSyncExternalStore — надёжная подписка на внешние сторы
useSyncExternalStore предназначен для библиотек и позволяет подписываться на внешние хранилища (сторы) и безопасно получать их снимок состояния как для клиентского, так и для серверного рендеринга.
Сигнатура:
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);Параметры:
- state: возвращаемое текущее значение стора;
- subscribe: функция регистрации колбэка при изменении стора;
- getSnapshot: функция, возвращающая текущее значение стора;
- getServerSnapshot: (опционально) снимок для серверного рендеринга.
useSyncExternalStore упрощает поддержку консистентного поведения при SSR и гидрации, снижая риск рассинхронизации состояния.
useInsertionEffect — оптимизация для CSS-in-JS
useInsertionEffect — хук для библиотек, добавляющих стили в DOM (CSS-in-JS). Он позволяет вставлять стили в DOM перед тем, как React будет читать и измерять макет (useLayoutEffect), что помогает избежать мерцаний и проблем с порядком подключения стилей.
Используйте этот хук в коде библиотек, которые создают теги