Оригинальная статья опубликована в блоге React, повествование в переводе ведётся от имени авторов оригинала Дэна Абрамова (Dan Abramov) и Рэйчел Нэйборс (Rachel Nabors).
- Обошлись без новой функциональности
- Постепенные обновления
- Пример постепенных обновлений
- Изменения делегирования событий
- Другие критические изменения
- Согласованные ошибки при возврате undefined
- Нативные стеки компонентов
- Удаление приватного экспорта
- Установка
10 августа мы выпустили первый релиз-кандидат (предварительную версию) React 17. С момента последнего мажорного обновления React прошло 2.5 года, а это много по нашим стандартам. В этой статье расскажем, какова роль этого обновления, каких изменений от него ожидать и как протестировать предварительную версию React 17.
Обошлись без новой функциональности
Версия React 17 необычная, потому что в ней нет каких-либо новых возможностей для разработчиков. Вместо этого новая версия позволяет удобнее обновлять сам React.
Мы активно работаем над новыми возможностями, но они не войдут в эту версию. React 17 — часть нашей стратегии по внедрению новой функциональности. Нам важно, чтобы она была доступна всем пользователям независимо от версии React, которую они используют.
В частности, React 17 — «релиз-ступень», которая обеспечивает безопасное встраивание дерева, управляемого одной версией React, в дерево, которое управляется другой версией React.
Постепенные обновления
В последние 7 лет обновления версий React работали по принципу «всё или ничего». Вы либо работаете со старой версией, либо обновляете приложение, и оно полностью работает на новой версии. Промежуточных вариантов не было.
Пока это работает, но стратегия «всё или ничего» ограничивает нас. Некоторые изменения API, например, отказ от устаревшего API Context, невозможно выполнить автоматически. Большинство написанных в последнее время приложений не используют устаревшие API, но мы всё равно поддерживаем их. Мы вынуждены выбирать межу поддержкой устаревшей функциональности в React и необходимостью навсегда оставить некоторые приложения на старых версиях React. Оба варианта неудачные.
Поэтому мы сделали новую опцию.
React 17 позволяет обновляться постепенно. Когда вы обновляетесь с версии 15 на 16 (или, в будущем, с версии 16 на 17), обычно вы обновляете всё приложение. Это отлично работает во многих случаях. Но такой подход может стать проблемой, если кодовая база была написана несколько лет назад и не поддерживалась. Использовать разные версии React на одной странице можно было и раньше. Но до React 17 такой подход делал код хрупким и вызывал проблемы с событиями.
Мы решили многие из этих проблем с помощью React 17. То есть после выхода React 18 и других версий в будущем у вас появится больше возможностей. Одна из них — обновлять приложение полностью, как это происходило раньше. Но у вас появится и возможность обновлять приложение по частям. Например, вы сможете обновить приложение до будущей версии React 18, но оставить на React 17 некоторые диалоги или вложенные роуты с отложенной загрузкой.
Это не значит, что вы должны использовать постепенные обновления. Лучшей стратегией для большинства приложений по-прежнему будет одномоментный переход на новую версию. Загрузку двух версий React нельзя назвать хорошим идеальным решением, даже если одна из них загружается лениво по требованию. Но для больших приложений, которые не поддерживаются активно, этот подход имеет смысл. А React 17 позволяет таким приложениям не устаревать безнадёжно.
Чтобы использовать поэтапное обновление, вам нужно внести некоторые изменения в систему обработки событий React. React 17 — мажорный релиз, так как реализованные в нём изменения потенциально критические. На деле нам пришлось изменить менее чем 20 компонентов из более чем 100 тыс. Поэтому мы ожидаем, что большинство приложений смогут мигрировать на новую версию без серьёзных проблем. Сообщите нам, если вы всё-таки столкнулись с проблемами при обновлении.
На Хекслете курс по React входит в профессию «Фронтенд JavaScript». После регистрации базовые курсы в профессии, включая «Введение в программирование», «Основы командной строки», «Настройка окружения», «Системы контроля версий», можно пройти бесплатно.
Пример постепенных обновлений
Мы подготовили демо-репозиторий, в котором можно посмотреть, как при необходимости может быть реализована ленивая загрузка старой версии React. В этом репозитории используется бойлерплейт Create React App, но вы можете использовать любой другой инструмент. Пулреквесты с примерами использования других инструментов приветствуются.
Важно
Другие изменения мы отложили, они появятся после релиза React 17. Цель React 17 — сделать возможными постепенные обновления. Если обновление до React 17 было бы сложным, мы не достигли бы поставленной цели.
Изменения делегирования событий
Создание приложений с использованием разных версий React всегда было технически возможным. Однако такой подход был довольно хрупким из-за внутреннего устройства системы обработки событий.
Обычно вы вешаете обработчики событий на элементы в React-компонентах так:
<button onClick={handleClick}>
Эквивалентный код на чистом JavaScript выглядит так:
myButton.addEventListener('click', handleClick);
Но в большинстве случаев React не привязывает события к узлам DOM, на которых вы их определили. Вместо этого React привязывает один обработчик на каждый тип события прямо к document
. Это называется делегированием событий. Оно повышает производительность больших приложений.
Делегирование событий используется в React с момента появления библиотеки. Когда возникает событие в DOM, React определяет, какой компонент вызвать, а затем событие React всплывает через ваши компоненты. Но под капотом нативное событие уже всплыло на уровень document
, где установлены обработчики событий React.
Однако это приводит к проблемам при постепенном обновлении.
Если вы используете на странице несколько версий React, каждая из них регистрирует обработчики событий на верхнем уровне. Это нарушает e.stopPropagation()
: если вложенное дерево остановило всплытие события, внешнее дерево всё ещё получает его. Из-за этого сложно использовать разные версии React одновременно. Это не гипотетический пример — разработчики редактора Atom столкнулись с этой проблемой на практике несколько лет назад.
Именно поэтому мы поменяли внутреннее устройство привязки событий в React 17.
В React 17 событие больше не привязывается на уровне document
. Вместо этого React привязывает его к контейнеру DOM, в котором отрисовывается ваше React-дерево.
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
В React 16 и более ранних версиях используется document.addEventListener()
. React 17 вместо этого использует под капотом rootNode.addEventListener()
.
Благодаря этим изменениям теперь стало безопаснее встраивать React-дерево под управлением одной версии библиотеки внутрь дерева, которым управляет другая версия библиотеки. Чтобы это работало, обе части приложения должны использовать React 17 и выше. Поэтому обновление до React 17 играет важную роль. Говоря иначе, новый релиз — необходимая ступень, которая сделает возможными постепенные обновления.
Также описанные выше изменения позволяют проще встраивать React в приложения, созданные с использованием других технологий. Например, если внешняя «оболочка» вашего приложения написана на jQuery, но внутри более новый код написан на React, e.stopPropagation()
внутри React-кода не даст событию достигнуть jQuery-кода, как вы и ожидаете. Это работает и в обратном направлении. Если вам больше не нравится React и вы хотите переписать приложение, например, на jQuery, можете переписывать внешнюю «оболочку» с React на jQuery, не опасаясь нарушить всплытие событий.
Мы подтвердили, что многочисленные проблемы, связанные с интеграцией React с другими технологиями, решаются благодаря новому поведению.
Важно
Вы можете спросить, как обновление повлияло на использование порталов за пределами рут-контейнера. React также отслеживает события, привязанные к контейнерам порталов, поэтому проблем здесь нет.
Устранение потенциальных проблем
Как и в других случаях выхода критических обновлений, возможно, вам придётся скорректировать код приложений. В Facebook нам пришлось изменить около 10 модулей, а всего у нас много тысяч модулей.
Например, если вы добавляете кастомные прослушиватели событий с помощью document.addEventListener(...)
, можете ожидать, что они перехватят все события React. В React 16 и более ранних версиях даже при использовании e.stopPropagation()
в обработчике событий React кастомные прослушиватели всё равно получат события, потому что нативные события уже находятся на уровне document
. При использовании React 17 всплытие событий будет остановлено, что и требуется, поэтому ваши обработчики на уровне document
не получат их.
document.addEventListener('click', function() {
// Этот обработчик больше не будет срабатывать на событиях
// из React-компонентов, которые вызывают e.stopPropagation()
});
Вы можете решить проблему, если привяжете прослушиватель к стадии погружения. Чтобы сделать это, надо передать { capture: true }
третьим аргументом в document.addEventListener
:
document.addEventListener('click', function() {
// Теперь обработчик привязан к стадии погружения,
// поэтому он получает все события click, определённые ниже
}, { capture: true });
Обратите внимание, насколько в целом гибкий этот подход. Например, использование стадии погружения вероятно исправит ошибки в вашем коде, которые происходят при вызове e.stopPropagation()
вне обработчика событий. Иными словами, всплытие событий в React 17 больше похоже на всплытие событий в DOM.
Другие критические изменения
Мы минимизировали количество критических обновлений в React 17. Например, в новой версии сохраняются все методы, которые были объявлены устаревшими в предыдущих версиях. Однако в React 17 есть другие критические обновления. Наш опыт показал относительную безопасность таких обновлений. В целом, нам пришлось изменить код менее чем в 20 модулях из более чем 100 000 тысяч.
Согласованность с браузерами
Мы внесли несколько изменений в систему событий:
- Событие
onScroll
не всплывает, это позволяет решить распространённую проблему — срабатывание обработчика на родительском элементе при скроле дочернего элемента. - События React
onFocus
иonBlur
теперь используют нативные событияfocusin
иfocusout
под капотом. Это лучше подходит к существующему поведению React и иногда даёт дополнительную информацию. - Стадия погружения событий (например,
onClickCapture
) сейчас использует реальные браузерные прослушиватели событий.
Эти изменения согласовывают поведение React с поведением браузеров и улучшают их взаимодействие.
Важно
Хоть React 17 под капотом перешёл с focus
на focusin
для события onFocus
, помните, что это не повлияло на поведение при всплытии. В React событие onFocus
всегда всплывало, и в новой версии это не меняется, так как в целом такое поведение более полезное. В этом примере можно увидеть разные проверки, которые можно использовать с разными вариантами использования события.
Отказ от использования пулов событий
В React 17 мы отказались от оптимизации с помощью объединения событий в пулы. Она не улучшает производительность в современных браузерах и создаёт проблемы даже для опытных React-разработчиков.
function handleChange(e) {
setData(data => ({
...data,
// Это не работает в React 16 и более ранних версиях:
text: e.target.value
}));
}
Это связано с тем, что React переиспользовал объект событий с разными событиями для повышения производительности в старых браузерах и очищал их свойства после вызова обработчика. В React 16 и более ранних версиях необходимо использовать e.persist()
, чтобы извлечь событие из пула и использовать его.
В React 17 этот код работает так, как вы ожидаете. Мы полностью отказались от использования пулов событий, поэтому вы можете обратиться к полям событий в любое время, когда это необходимо.
Это изменение поведения, поэтому мы отметили это обновление как критическое. На практике мы не увидели, чтобы оно вызвало какие-то проблемы в коде Facebook. Возможно, обновление даже исправило какие-то ошибки. Заметьте, что e.persist()
всё так же доступен в объекте события React, но сейчас он ничего не делает.
Тайминг сброса эффектов
Мы сделали тайминг функции сброса useEffect
более согласованным.
useEffect(() => {
// Это эффект
return () => {
// Это сброс.
};
});
Большинству эффектов не нужно откладывать обновление экрана, поэтому React выполняет их асинхронно сразу после того, как экран обновляется. В редких случаях, когда вам нужен эффект для блокирования отрисовки, например, для измерения и позиционирования всплывающей подсказки, используйте useLayoutEffect
.
В React 16 функция сброса эффекта, если она существует, выполняется асинхронно. Мы заметили, что это неидеально для больших приложений, так как замедляет переходы на больших экранах, например, при переключении вкладок.
В React 17 функция сброса эффекта тоже запускается асинхронно. Например, если компонент размонтируется, функция сброса выполнится после обновления экрана.
Это отражает принцип работы самих эффектов. В редких случаях, когда вы рассчитываете на синхронное выполнение, можно использовать useLayoutEffect
.
Важно
Значит ли это, что теперь невозможно исправлять предупреждения о setState
на размонтированных элементах? Не переживайте, React специально проверяет это и не запускает предупреждения о setState
в промежутке между размонтированием и сбросом. Поэтому запросы отмены или интервалы почти всегда остаются одинаковыми.
Также React 17 выполняет функции сброса в таком же порядке, что и эффекты — в соответствии с их расположением в дереве. В более ранних версиях этот порядок мог изменяться.
Устранение потенциальных проблем
Мы заметили, что это обновление сломало только несколько компонентов из тысяч, хотя ещё нужно тщательно протестировать переиспользуемые библиотеки. Один из примеров проблемного кода выглядит так:
useEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
Проблема в том, что someRef.current
мутабельный, поэтому к моменту запуска функции сброса его значение может быть null
. Чтобы решить эту проблему, можно фиксировать любые мутабельные значения внутри эффекта.
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
Мы не думаем, что эта проблема станет распространённой, так как о ней предупреждает линтер. Убедитесь, что используете правило eslint-plugin-react-hooks.
Согласованные ошибки при возврате undefined
В React 16 и более ранних версиях возврат undefined
всегда был ошибкой.
function Button() {
return; // Ошибка: render ничего не возвращает.
}
Это частично связано с тем, что undefined
легко вернуть непреднамеренно.
function Button() {
// Мы забыли про return, поэтому компонент возвращает undefined.
// React считает это ошибкой, а не игнорирует.
<button />;
}
Раньше React проверял возвращаемые значения из функциональных компонентов и компонентов на классах, но не проверял компоненты forwardRef
и memo
. Это связано с ошибкой в коде.
В React 17 компоненты forwardRef
и memo
ведут себя так же, как компоненты на классах и функциональные компоненты. При возврате из них undefined
вы получите ошибку.
let Button = forwardRef(() => {
// Мы забыли про return, поэтому компонент возвращает undefined.
// React 17 считает это ошибкой, а не игнорирует.
<button />;
});
let Button = memo(() => {
// Мы забыли про return, поэтому компонент возвращает undefined.
// React 17 считает это ошибкой, а не игнорирует.
<button />;
});
В ситуациях, когда вы намеренно не хотите ничего отрисовывать, используйте null
.
Нативные стеки компонентов
Когда вы бросаете ошибку в браузере, он показывает вам трассировку стека (stack trace) с названием функций и их расположением. Однако стеков JavaScript часто не хватает для отслеживания проблем, так как здесь играет важную роль иерархия дерева React. Вам необходимо знать не только то, что компонент <Button>
выбросил ошибку, но также где в дереве React находится этот компонент.
В связи с этим начиная с версии React 16 при возникновении ошибки печатается стек компонентов. Раньше стек компонентов был менее удобным по сравнению с нативным стеком JavaScript. В частности, стек компонентов в консоли не был кликабельным, так как React не знал, где в исходном коде объявлена конкретная функция. Кроме того, стеки компонентов были практически бесполезными при работе с продакшен-кодом. В отличие от обычных минифицированных стеков JavaScript, которые позволяют получить имена функций с помощью маппинга, при использовании стеков компонентов React приходилось выбирать между размером бандла и использованием стеков в продакшене.
В React 17 мы используем другой механизм для создания стеков. Стеки компонентов создаются из нативных стеков JavaScript. Это позволяет получить удобные трассировки стеков React-компонентов в продакшен-окружении.
React обеспечил это с помощью подхода, который нельзя назвать традиционным. В настоящее время браузеры не дают доступ к стековому кадру (стек-фрейм, stack frame) функции (исходному файлу и расположению). Когда React перехватывает ошибку, он будет восстанавливать стек компонентов, выбрасывая и перехватывая временную ошибку внутри каждого из компонентов, которые находятся на более высоком уровне. Это незначительно снижает производительность при сбоях, но такое происходит только один раз на каждый тип компонентов.
Если хотите больше подробностей, посмотрите этот пулреквест. В большинстве случаев этот механизм не должен повлиять на ваш код. Разработчикам будет полезно знать о новой возможности: стеки компонентов теперь стали кликабельными, так как они созданы на основе нативных браузерных стек-фреймов. Это позволяет «расшифровать» их и найти нужную информацию об ошибках так же, как вы делаете это с обычными ошибками JavaScript.
Критичность изменения заключается в том, что React после перехвата ошибки повторно запускает часть функций и конструкторов классов, которые расположены в стеке выше. Поскольку функции рендеринга и конструкторы классов не должны иметь побочных эффектов, что важно для серверного рендеринга, это не должно привести к проблемам на практике.
Удаление приватного экспорта
Последним важным критическим изменением можно назвать удаление некоторых внутренних реализаций React, которые ранее были доступны другим проектам. Например, раньше React Native for Web зависел от внутренних реализаций системы событий. Но эти зависимости были хрупкими и часто ломались.
В React 17 мы удалили такие приватные экспорты. Насколько нам известно, React Native for Web был единственным проектом, который их использует. И этот проект уже использует другие инструменты, которые не зависят от этих приватных экспортов.
Это значит, что старые версии React Native for Web будут несовместимыми с React 17. Но у новых версий проблемы совместимости не будет. На практике это не вызовет затруднений, так как React Native for Web и так должен был обновляться, чтобы не терять совместимость после изменений React.
Кроме того, мы удалили вспомогательные методы ReactTestUtils.SimulateNative
. Они не документировались, их названия не соответствовали их функциям, и они не соответствовали изменениям в системе событий. Если вам нужен надёжный способ использовать нативные браузерные события в тестах, обратите внимание на React Testing Library.
Установка
Рекомендуем в ближайшее время установить предварительную версию React 17 и сообщать о любых проблемах, с которыми вы столкнётесь во время миграции. Помните, что по сравнению со стабильной версией в предварительной более вероятны ошибки. Поэтому пока не используйте релиз-кандидат в продакшене.
Чтобы установить предварительную версию React 17 из npm, используйте команду:
npm install react@17.0.0-rc.0 react-dom@17.0.0-rc.0
Для установки из Yarn используйте команду:
yarn add react@17.0.0-rc.0 react-dom@17.0.0-rc.0
Также через CDN доступны UMD-сборки:
<script crossorigin src="https://unpkg.com/react@17.0.0-rc.0/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17.0.0-rc.0/umd/react-dom.production.min.js"></script>
Адаптированный перевод статьи React v17.0 Release Candidate: No New Features by Dan Abramov and Rachel Nabors.