Disclamer
Это вольный перевод оригинальной статьи из официального блога. Почему вольный? Скорее потому, что в оригинале слишком много воды и отсылок к причинам тех или иных принятых в прошлом решений.
Ничего нового?
Семнадцатый релиз React необычен отсутствием новых фич и/или функционала. Этот релиз сфокусирован на добавлении возможности постепенного обновления проектов на следующие мажорные версии библиотеки в будущем, что актуально для проектов с большой кодовой базой.
Многообещающий Concurrent Mode не будет представлен в 17 версии, как и другие нововведения, над которыми активно работает команда. Грядущий релиз является частью стратегии постепенных (частичных) обновлений.
Постепенные обновления
В течение последних 7 лет обновления React были в духе "все или ничего". Либо обновляемся до новой версии, либо остаемся на старой. Порой, необходимо было менять что-то в кодовой базе, как, например будет с устаревшим Context API, который не получится перенести автоматически.
React 17 привносит стратегию постепенных обновлений. Команда чинит большое количество имеющихся проблем в React 17. Это позволяет в следующей версии библиотеки иметь больше опций при обновлении.
Первый из вариантов будет таким же как и прежде: обновляем все приложение разом. Но теперь будет опция обновить приложение по частям. Например, большую часть приложения легко перетащить на новую версию (React 18, или React 19), тогда как какое-нибудь загруженное через lazy-подход диалоговое окно или часть приложения под определенном роутом можно будет оставить на React 17.
Команда приготовила репозиторий, который демонстрирует как загружать асинхронно (lazy-load) более старые версии React. В примере используется приложение на React 17.0.0-rc.0, которое подгружает компоненты, написанные с помощью устаревших приемов, работающие на React 16.8
Список изменений в React 17
Изменение в делегировании событий
Реакт с самой его первой версии меняет способ привязки событий, например onClick
, к DOM-элементам. Реакт автоматически использует прием делегирования событий и привязывает все события к объекту document
. Таким образом, достигается повышение производительности.
Однако, в случае, если несколько версий React используется на странице, у вас микрофронтенды или все еще часть функционала работает на jQuery, возникают проблемы. Такое поведение ломает event.stopPropagation()
: если вложенное дерево остановило распространение (propagation) события, внешнее дерево все равно получит его. Это делает сложным работу в случае вложенных нескольких версий React. Команда популярного редактора Atom столкнулась с такой проблемой.
Теперь все обработчики крепятся к корневому элементу, а не объекту document:
const rootNode = document.getElementById('root'); // <-- вот сюда
ReactDOM.render(<App />, rootNode);
Убран костыль с Синтетическим Событием (SyntheticEvent Even Pooling)
В 17-м Реакте убрана оптимизация событий, которая более не актуальна в современных браузерах.
function handleChange(event) {
// это работает в 16 React только если добавить event.persist()
setData(data => ({
...data,
// This crashes in React 16 and earlier:
text: event.target.value
}));
}
Теперь такой код не будет валиться с ошибкой, и нет нужды писать event.persist()
Для обратной совместимости эта функция оставлена в качестве заглушки. Разработчики попробовали это на существующем коде в Facebook и увидели отсутствие регрессий. Возможно, это обновление еще и исправило какое-то количество багов!
Ближе к браузерам
Было сделано некоторое количество небольших изменений. Эти изменения сближают React с поведением браузера.
Событие
onScroll
больше не всплывает, чтобы избежать текущей путаницы;События
onFocus
иonBlur
изменены "под капотом" на нативныеfocusin
иfocusout
;onClickCapture
и другие Capture-события теперь используют браузерные обработчики событий.
useEffect() теперь полностью асинхронный
useEffect(() => {
// This is the effect itself.
return () => {
// This is its cleanup.
};
});
Если раньше только функция эффекта запускалась асинхронно, тогда как возвращаемая функция для очистки подписок запускалась синхронно, так же как и метод componentWillUnmount()
, то теперь и эта функция работает асинхронно, что улучшает производительность в случае перерисовки большого количества элементов, например, когда на сайте присутствуют вкладки, или при переходе со страницы на страницу.
Для синхронной работы, можно по-прежнему использовать useLayoutEffect()
, который остался незатронутым.
Ошибки при возвращении undefined
Это изменение довольно минорно, но повышает консистентность ошибок.
Если раньше нельзя было возвращать undefined
только в обычных компонентах, то теперь такая ошибка будет выбрасываться еще и в React.forwardRef
и React.memo
.
let Button = forwardRef(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
<button />;
});
let Button = memo(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
<button />;
});
Улучшенный стек вызовов при ошибках
Когда выбрасывается ошибка, браузер показывает stack trace с названиями функций и их местоположения. К сожалению, довольно трудно по этому стеку вызовов понять, в каком месте и в каком компоненте была вызвана ошибка на самом деле.
Хочется узнать, что не просто компонент Button
вызывал ошибку, но и в каком месте дерева React-компонентов эта кнопка находилась.
В 17-м React используется новых механизм по генерации стека вызовов, что позволяет увидеть дерево React-компонентов, которое привело к ошибке, даже в production-среде.
Удаление приватных экспортов
Последнее изменение — это удаление некоторых внутренних экспортов, которые ранее были открыты наружу. Например, React Native for Web ранее зависела на некоторых внутренностях системы событий, но эта зависимость не была надежной.
В 17-м React эти приватные экспорты были удалены. По большей части из-за того, что только вышеупомянутый проект их использовал, а миграция на более надежные методы уже была произведена.
Также был удалены ReactTestUtils.SimulateNative
методы. Они не были документированы, но теперь их не будет вовсе.
Changelog
Переводить каждый пункт в логе изменений не имеет смысла, множество из них слишком минорны, а об основных было рассказано выше.
В новой версии React также включено 5 изменений в React, 37 изменений в React Dom, пару изменений в React DOM Server, одно изменение в React Test Rerender.
А что с Concurrent Mode?
По-прежнему, этот режим имеет статус экспериментального. В 17-м React было исправлено множество багов, удалены одни unstable_
методы, и добавлены новые. Пока для продакшена использовать его рано, но потыкать определенно можно и нужно. Например, есть библиотека для работы с Firebase, reactfire, разработчики которой сделали основную версию зависимой от Concurrent Mode. К сожалению, репозиторий кажется заброшенным последние несколько месяцев. Надеюсь, это исправится.
JustDont
Люблю наблюдать, как натягивание какой-то абстракции на имеющийся домен внезапно рушит вещи, которые в абстракцию не очень вписываются. Это даже не про реакт 17, а про реакт вообще. Скажем, вот есть у нас модель, схематично описываемая объектом со структурой a.b.c, и соответствующие вкладывающиеся друг в друга реакт-компоненты A, B, и С. Что можно сделать в парадигме реакта, чтоб перерисовать B и только B? А ничего. Даже не смотря на то, что при неизменности C не произойдет собственно обновления соответствующего узла DOM, это очень слабое утешение, потому что море бесполезной работы (рендер в VDOM, сравнение VDOM и DOM) всё равно будет безусловно проделано. Что очень даже больно, если у вас не один C, а тысячи.
Всё, что может предложить в ответ реакт — это «делите компоненты», и вот мне уже нужно иметь компонент «обертка (списка) C», который не имеет никакого смысла для моей модели UI и нужен исключительно для того, чтоб реакт мог «догадаться», что эта часть дерева компонентов не поменялась. Просто потому, что концепция частичных изменений (которую очень легко реализовать с vanillaJS, потому что взаимодействие с DOM идёт по отдельным элементам, очень легко не трогать тот DOM, который трогать не надо) в реакте просто не ночевала.
И это я даже не говорю про то, что даже после всего дробления всё равно нужно выполнять довольно неочевидные действия в модели (новый объект b, указывающий на старый (список) c) просто потому, что реакт предлагает исключительно referential equality в деле сравнения пропсов.
ЗЫ: А, так вот. Вангую, что с concurrent mode масса реактопрограммистов просто будет дергать эти самые перерисовки длинных списков везде, где не надо (как это сейчас уже происходит), и будет радоваться тому, что браузер больше не подвисает и вообще всё шоколадно. А юзеры на не очень крутых машинах будут наблюдать, как у них куски страниц неторопливо и торжественно обновляются по частям туда-сюда прямо на глазах. Зато браузер не подвисает, ага.
Carduelis Автор
Строго говоря, с помощью React и соответствующего драйвера можно рисовать что угодно и где угодно. Хоть на Canvas, хоть в командной строке. Но это все в теории, а на практике у нас SyntheticEvent и Concurrent Mode.
Я думаю, в каком-то новом React будет более близкое API к браузеру. Может быть даже и работа напрямую с DOM там, где это необходимо. Shadow DOM, опять же, пока React обходит стороной.
Одно радует, что API React меняется, не держится сильно за старое, и есть надежда на светлое будущее =)
rewlad
Или в каком-то новом браузере будет вменяемое API близкое к React, с реализацией на расте.
GemsGame
Ну ты можешь убрать лишние рендеры, если тебе это нужно
const Header = ({title}) => {title}
export default React.memo(Header);
JustDont
Это и есть создание никому (кроме реакта) не нужных обёрток.
Carduelis Автор
Вы так говорите, как будто в этом есть что-то плохое.
Все программирование — это обертки. Обертки над обертками. Или абстракции.
В данном случае мы жертвуем унификацией использования этой абстракции производительностью.
В следующий раз, как вы будете писать какое-нибудь замыкание, задумайтесь о никому не нужных обертках =)
JustDont
Таки да, в этом очень много плохого. Когда обертки порождены невыразительностью абстракций самого фреймворка — это говорит только о том, что фреймворк не так уж и хорош.
Это особенно хорошо видно, стоит только лишь взять любой нормальный FRP (от того же MobX или RxJS до, скажем, той реактивности, что из коробки предлагает Svelte), и обнаружить, что с нормальными абстракциями лишние обертки не появляются; да и описывать изменения моделей без принудительной referential equality гораздо легче.
DDroll
Простите за оффтоп, но ждал появления Svelte еще в первом вашем комменте. Когда вижу кого-то, кто цепляется к каким-то незначительным недостаткам ведущей тройки фреймворков (на уровне вкусовщины), а потом огромными комментами с использованием множества аббревиатур и англицизмов пытается раздуть их до астрономических размеров, понимаю — передо мной поклонник этого молодого фреймворка)
JustDont
Кривые идеи в основе реакта — это конечно же незначительные недостатки, полностью с вами согласен.
Carduelis Автор
А напишите статью про кривые идеи в основе реакта!
Было бы интересно это еще и подкрепить мотивацией авторов для добавления таких кривых идей. Для большей объективности.
С радостью бы почитал такое и улучшил скилл мета-программирования.
JustDont
Вот одну уже без меня написали.
SQReder
Посмотрел по-диагонали. Этот странный человек борется с пробросом пропсов написанием странного костыля, вместо того, что-бы использовать React.Context. И где-то походя ругает редукс.
Вопрос что произойдет со ссылкой на компонент после анмаунта, как я понял, не рассматривается даже.
JustDont
«Решение», которое автор предлагает — довольно кривое, и его за это уже там покритиковали все кому не лень. Но я не про решение, а про проблему, которую он описывает, если уж мы говорим о проблемах реакта. Проблема описывается самая что ни на есть животрепещущая.