В начале лета в официальном блоге React вышла новость о планах на 18 версию библиотеки. Команда разработчиков рассказала о новых фичах и API, которые планируется зарелизить в обновлении. Меня зовут Ильмир Шайхутдинов, я frontend разработчик Технократии, и в этой статье я хочу пробежаться по изменениям и познакомить вас с ними поближе.
Содержание
Публичная рабочая группа
Автоматический батчинг
Strict mode
Root API
Конкурентный рендеринг
Новые API
Публичная рабочая группа
Процесс создания обновления сделали публичным. За прогрессом рабочей группы можно следить на платформе Github Discussions. Посмотреть обсуждения может каждый, но доступ к комментированию и созданию новых тем есть только у команды React и активных участников сообщества (преподаватели, разработчики и авторы библиотек). Благодаря этому появилась возможность заглянуть за кулисы разработки и раньше релиза ознакомиться с изменениями в React 18. Перейдем к ним.
Автоматический батчинг
React 18 добавляет возможность автоматического батчинга обновления состояний для асинхронных операций: promise, таймауты, fetch запросы. Батчингом в React называют процесс группировки нескольких вызовов обновления состояния в один этап ререндера. Это положительно сказывается на производительности.
До React 18 батчинг также существовал, но автоматически работал только для обработчиков DOM событий:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // Не вызывает ререндер
setFlag(f => !f); // Не вызывает ререндер
// React вызовет ререндер только один раз, в конце
}
/*
function handleClick() {
fetchSomething().then(() => {
// До React 17 следующие вызовы не батчились
// Установка состояния происходит “после” события, в колбэке асинхронного вызова
setCount(c => c + 1); // Спровоцирует ререндер
setFlag(f => !f); // Спровоцирует ререндер
});
}
*/
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
В React 18 все обновления состояния внутри Promise, таймаутов, fetch-запросов будут батчиться также, как для обработчиков DOM-событий. Код ниже:
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React будет вызывать ререндер только один раз, в конце
}
//работает так же, как и этот код:
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React будет вызывать ререндер только один раз, в конце
}, 1000);
Как отменить батчинг?
Обычно батчинг безопасен и не вызывает проблем при разработке, но если сразу после обновления состояния нужно прочитать изменения в DOM, то можно использовать ReactDOM.flushSync() для отмены батчинга:
import { flushSync } from 'react-dom'; // Внимание: react-dom, не react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React вызовет ререндер
flushSync(() => {
setFlag(f => !f);
});
// React вызовет ререндер
}
Более подробно про автоматический батчинг в теме на Github.
Strict mode
Следующей фичей React 18 станет улучшение режима Strict Mode. В него добавится новый режим под названием “strict effects”. Чтобы понять, что это за режим, вспомним, как Strict Mode работал до обновления.
Компоненты, обернутые в <StrictMode> (только в dev режиме), умышленно рендерятся по два раза, чтобы избежать нежелательных сайд-эффектов, которые можно добавить в процессе разработки.
С релизом React 18 в StrictMode добавляется новое поведение — “strict effects”. С ним эффекты для вновь смонтированных компонентов вызываются дважды (mount -> unmount -> mount). Дополнительный вызов эффекта не только обеспечивает устойчивую работу компонента, но и необходим для правильной работы Fast Refresh, когда компоненты монтируются/размонтируются при обновлении в процессе разработки. Также это необходимо для работы новой фичи Offscreen API, находящейся в разработке.
Offscreen API
Она улучшает производительность таких компонентов как виртуальный скролл, табы, а также внедряет поддержку браузерного API — content-visibility.
Чтобы понять принцип работы Offscreen API, рассмотрим пример. У вас есть компонент, рендер которого происходит по определенному условию. Например, текущий таб. У данного компонента есть внутреннее состояние, и если мы сменим таб, то оно пропадет вместе с размонтированием (unmount) данного компонента.
Основная цель Offscreen API — возможность сохранять внутреннее состояние компонента, скрывая его, а не размонтируя. Для этого React вызывает методы жизненного цикла, например, unmount, но вместе с этим сохраняет внутреннее состояние компонента и его DOM элементы. Таким образом, в процессе жизни компонента событие “mount” и “unmount” могут вызываться несколько раз.
Более подробно про “strict effects” и Offscreen API в теме на Github.
Root API
Идем дальше. В обновлении нас ждут новый Root API и старый (legacy) Root API. Команда React специально оставила старый Root API, чтобы пользователи, которые обновили версию, могли постепенно перейти на новую, сравнивая при этом ее работу со старой. Использование старого Root API будет сопровождаться предупреждением в консоли о необходимости переключения на новый. Рассмотрим пример с новым Root API и увидим разницу с текущей реализацией:
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// До
ReactDOM.render(<App tab="home" />, container);
// После
const root = ReactDOM.createRoot(container);
root.render(<App tab="home" />);
Теперь отдельно создается “корень” — указатель верхнеуровневой структуры данных, которую React использует для отслеживания дерева для рендеринга. В предыдущих версиях React “корень” был недоступен для пользователя, React прикреплял его к DOM-узлу и никуда не возвращал. В новом Root API изменился метод гидратации контейнера. Теперь вместо hydrate нужно писать hydrateRoot:
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// До
ReactDOM.hydrate(<App tab="home" />, container);
// После
// Создание и рендер с гидратацией
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// В отличие от createRoot(), не нужно отдельно вызывать root.render()
Обратите внимание на порядок аргументов в hydrateRoot(): он принимает JSX вторым аргументом. Это сделано из-за того, что первый рендер клиента является особенным и требует соответствия с серверным деревом.
Если надо обновить “корень” приложения после гидратации, можно сохранить его в переменную и вызывать render():
import * as ReactDOM from 'react-dom';
import App from 'App';
const container = document.getElementById('app');
// Создаем и рендерим корень с гидратацией
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// Обновляем корень приложения
root.render(<App tab="profile" />);
Более подробно про Root API в теме на Github.
Конкурентный рендеринг
Конкурентный режим внедряется постепенно. Команда разработки дала пользователям возможность частичного использования конкурентных фич. Таким образом, можно обновиться до React 18 и практически ничего не изменять в коде приложения — все будет работать. React 18 поддерживает плавное обновление кодовой базы приложения.
На всякий случай определим, что такое конкурентный режим. Он предназначен для более плавной работы приложения на устройстве пользователя. Одна из областей, где данная фича применяется, это прерываемый рендеринг. Представьте, что пользователь вводит в строку поиска текст. Это событие обновляет состояние компонента, и происходит рендер нового списка результатов. Во время этого процесса залипает ввод: браузер не может обновить введенный в поле текст, так как занимается рендером нового списка результатов. Конкурентный режим исправляет это ограничение, делая рендер прерываемым.
Новые API
С новыми фичами конкурентного рендеринга были добавлены и новые API. Они относятся к переходам состояний (state transition), фичам задержки (Suspense) и новым хукам.
startTransition
Это API добавлено для обновления состояния компонента, которое влечет за собой тяжелые вычисления, такие как фильтрация списка. Оно позволяет значительно улучшить пользовательский ввод и отклик интерфейса, помечая тяжелые обновления компонента как “переходы” (transitions).
API представлено в виде функции startTransition, в которую помещают обновление состояний, являющихся несрочными (non-urgent).
import { startTransition } from 'react';
// Срочное (urgent) обновление: отображаем введенный текст
setInputValue(input);
// Помечаем обновления состояний как переходы
startTransition(() => {
// Переход: фильтрация списка по введенному ключевому слову
setSearchQuery(input);
});
Обновления, обернутые в startTransition, помечаются несрочными и могут прерываться, если появятся более срочные обновления, спровоцированные пользовательским вводом. Если переход прервется пользователем, например, при вводе нескольких символов в строке, React отбросит незавершенную работу по рендерингу и отобразит только последнее обновление.
startTransition полезен, если вы хотите, чтобы пользовательский ввод был быстрым, не было фриза UI, а несрочные операции выполнялись на фоне. Помимо startTransition, появился новый хук useTransition. Он позволяет узнать статус перехода:
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
Если переход не завершен, то значение isPending будет равно true, поэтому можно отображать на UI спиннер, пока пользователь ждет:
{isPending && <Spinner />}
Более подробно про startTransition в теме на Github.
useDeferredValue
Данный хук вернет отложенную версию переданного значения, которая будет “отставать” от исходной на время, равное таймауту:
import { useDeferredValue } from "react";
// ...
const [text, setText] = useState("text");
const deferredText = useDeferredValue(text, { timeoutMs: 2000 });
Этот хук поможет в ситуациях, в которых нужно реализовать сложное отложенное поведение, завязанное на состояниях.
Улучшения Suspense
Suspense предназначен для отображения запасного интерфейса (спиннера) во время ожидания дочерних компонентов. Дочерние компоненты в это время могут выполнять асинхронные вызовы API либо загружаться через lazy load.
Основное нововведение в том, что фича стала стабильной, получила большие архитектурные изменения под капотом и приобрела название “Конкурентные задержки” (Concurrent Suspense). Смена названия никак не отразится на пользователях. Существенное изменение для пользователей заключается в рендере дочерних элементов внутри Suspense:
const App = () => {
return (
<Suspense fallback={<Loading />}>
<SuspendedComponent />
<Sibling />
</Suspense>
);
};
В React 17 компонент <Sibling /> будет смонтирован и вызваны его эффекты, затем он будет скрыт.
В React 18 это поведение исправлено: теперь компонент <Sibling /> смонтируется только после того, как <SuspendedComponent /> загрузится.
Более подробно про изменения Suspense в теме на Github.
SuspenseList
Предназначен для определения порядка, в котором загружаются и отображаются пользователю напрямую вложенные компоненты Suspense и SuspenseList.
<SuspenseList revealOrder="forwards">
<Suspense fallback={'Загрузка...'}>
<ProfilePicture id={1} />
</Suspense>
<Suspense fallback={'Загрузка...'}>
<ProfilePicture id={2} />
</Suspense>
<Suspense fallback={'Загрузка...'}>
<ProfilePicture id={3} />
</Suspense>
...
</SuspenseList>
Бывают случаи, когда на UI необходимо отобразить компоненты в определенном порядке, и если обернуть их в SuspenseList, то React не отобразит компонент, пока не загрузится предыдущий из списка (этим поведением можно управлять).
Более подробно, с описанием пропсов, в статье на сайте React.
Потоковый SSR
Были также внесены большие улучшения в Suspense Server-Side-Rendering (SSR). Рассмотрим основные фичи:
Выборочная гидратация.
React начнет гидратацию компонентов с учетом взаимодействия пользователя с содержимым сайта. Например, при клике на комментарий React приоритезирует гидратацию HTML родительских блоков:
Еще одна особенность в том, что React не будет блокировать UI во время гидратации — этот процесс будет происходить во время простоя браузера, поэтому пользовательские события будут обрабатываться сразу.
Потоковая отправка HTML
Позволяет отправить HTML клиенту без загрузки всех данных для рендера на сервер. А как только данные будут получены, они отрендерятся на сервере и отправятся на клиент. Например, есть блок с комментариями, и мы можем асинхронно загружать по ним информацию, отдавая HTML. А когда комментарии будут получены, отрендерить их и отправить клиенту в конце HTML документа. Выглядеть это будет примерно так:
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// Примерная реализация
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
Более подробно про потоковый SSR в теме на Github.
В этом разделе была описана лишь малая часть примеров работы с API конкурентного рендеринга. На сайте React написана статья с более подробными примерами и описаниями паттернов использования конкурентного рендеринга.
Дорожная карта
Уже сейчас можно протестировать новые изменения, установив версию под тегом @alpha, но будьте готовы к багам.
По информации из релизного плана, публичная бета версия и RC ожидаются в течение ближайших нескольких месяцев, а спустя 2-4 недели после RC выйдет стабильная версия React 18.
Таким образом, React 18 можно ожидать либо к концу 2021 года, либо в первой половине 2022 года.
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.