Горячая перезагрузка компонентов в React
У меня есть два любимых запроса в гугле:
— Что будет, если в унитаз поезда на полном ходу бросить лом?
— Что будет, если в реакту по полном ходу заменить компонент?
И если с первым вопросом все более менее понятно, то со вторым вопросом все не так просто — тут же вылезает Webpack-dev-server, Hot Module Replacement, и React-Hot-Loader.
И все бы хорошо, но эта гремечая просто не работает, спотыкаясь на первом же сложном коде, HOC, композиции, декораторе и нативных arrow функциях.
В общем признаем — третья версия Reac-hot-loader немного не удалась.
Преамбула
На самом деле у версии 3 было 3 проблемы:
— react-proxy, который использовался для «подмены» старой версии компонента новой, и который совсем не дружил с arrow функциями, к которым контекст прибит намертво.
— для обеспечения работы bound и transpiled arrow functions babel plugin оборачивал их в промежуточные функции. С учетом того, что он это делал для ВСЕХ классов — часто это приводило к поломкам. Те вообще все не работало.
— сам принцип поиска и подмены компонентов. Для этого использовался babel plugin, который «регистрировал» все переменные верхнего уровня, таким образом что потом можно было бы понять, что «вот эта переменная», на самом деле «БольшаяКпонка» и следует обновить прокси.
Все бы хорошо, но HOC может создать класс внутри себя, а декоратор просто «декорирует» реальную переменую. Как результат — реакт-реконсилер при рендере встретит «совершенно новый» класс на месте старого и размаутит все старое дерево.
Народная мудрость гласит:
One of the most annoying things to setup with Webpack is a properly working Hot Module Replacement. Working properly means that components will keep their local state. Everything else makes HRM uselss (in my opinion).
Другими словами — без нормального preserve state нафиг этот RHL не нужен.
Мое путешествие в RHL началось полгода назад, когда я примерно неделю (это очень много) пытался понять как все эти годы люди использовали React-hot-loader, потому что использовать его невыносимо больно. Ничего кроме боли он не приносит. А потом открыл PR, который молча стал подсказывать почему и где RHL размаунтит компонент, чтобы люди наконец смогли понять почему он не работает. Чтобы хоть как-то сгладить душевные страдания.
А потом решил починить все на корню.
Встречаем версию 4!
Буду краток — версия 4 работает практически всегда. У нее все еще есть ограничения, которые вытекают из самой природы явления, но эти «ошибки» — гарант правильности работы. Пробовать можно уже сейчас, просто обновите RHL до версии 4.
Что такое HMR, RHL и все такое?
HMR — горячая замена модулей. Встроенный в webpack и parcel механизм, который позволяет обновлять отдельные модули на клиенте, при их изменении на сервере. Только не забываем — это исключительно про разработку. Без дополнительных телодвижений молча отрефрешит страницу.
RHL — react-hot-loader, комплекс мер которые направлены на то, чтобы React просто перерендерил обновленный элемент, и не делал больше ничего.
RHL и есть то нечто, что обеспечивает «правильный» HMR в том виде, в каком он и нужен.
Есть альтернативная теория, гласящая что HMR ведет к monkey-patchингу, в лучше TDD, BDD и все такое. И большая такая капля правды в ней есть.
Отличия версии 4 от версии 3
1. Вместо react-proxy используется react-stand-in — чуть более «javascriptовое» решение. Основное отличие — standin это не «враппер», и не прокси — это класс который наследуется от реального.
Это очень хитрый момент — когда настанет время заменить один компонент другим — standin просто поменяет себе прототип (точнее прототип своего прототипа).
Результат — this никогда не меняется.
Второй хитрый момент — перенос изменений сделанных в конструкторе (в том числе добавленных в конструктор бабелем). Standin инстанцирует старый и новый класс, после чего ищет изменения и пробует их повторить.
В итоге — после обновления компоненты применяются все изменения, кроме сделанных в componentWill/DidMount. Потому что mount/unmount не происходил, и именно его и требовалось избежать.
2. Вместо «регистраций», которые не дружат с HOC и декораторами используется механизм названный «hotReplacementRender».
Когда наступает момент «обновления» компоненты React-Hot-Loader сохраняет старое дерево, и начинает сам рендерить новое.
После рендера каждого элемента наступает момент «сравнения» старых и новых данных, фактически реконсилер.
Если новый компонент «очень похож» на старый — возможно это он и есть, и можно произвести замену.
Такой подход без проблем пробивает любые композиции, декораторы, и вообще любые render-props. Единственный момент, который не переживает — изменение колличества или порядка следования детей (если не заданы key), но это вытекает уже из самой природы Reactа.
3. Раньше многие люди спотыкались на моменте настройки HRM и RHL. В новой версии вся настройка ограничивается _одной_ командой.
import React from 'react';
import {hot} from 'react-hot-loader';
const MySuperApplication = () => ......
export default hot(module)(MySuperApplication); <-----
Магия hot (module)(MySuperApplication) автоматически настроит HMR для текущего модуля, а HOC часть этой функции обернет MySuperApplication в AppContainer.
ВСЕ! (Плюс babel-plugin не забудьте)
Где работает? Везде — Webpack, parcel, typescript (babel-plugin не забудьте). Примеры есть на все.
Особенности работы, про которые лучше знать
1. RHL v3 оборачивал в прокси только «зарегистрированные» компоненты. v4 оборачивает абсолютно все. Теоритически это никак не должно сказываться на производительности, но всякое бывает.
2. Hot настраивает текущий модуль как self accepted. Тут надо понимать что ничего кроме «реакт-компоненты», который RHL может переживать экспортировать из модуля нельзя.
Стандарные сейчас ошибки:
— использовать hot для локальных переменных или вообще в рендере (детектим и ругаемся)
— использовать hot для HOC и функций — молча все ломается.
Hot — _только_ для компонент.
3. Если в componentDidMount вы запустили таймер, а потом изменили функцию которую этот таймер вызывает — она не обновиться. Потому что setTimeout уже получил ссылку на функцию, и поменять ее нельзя.
4. Code splitting — требуется или обернуть в hot каждый компонент который вы собираетесь динамически загружать, тогда он «сам себя» обновит, или использовать «загрузчик» который знаком с HMR/RHL.
— популярный react-lodable в принципе не подходит. Если он у вас — никаких других вариантов, крое как обернуть компоненты в hot — нет. (не забываем что это никак не сказывается на продакшене)
— loadable-components немного лучше — они попытаются обновить файл, но могут быть проблемы, так как «hotReplacementRender» будет уже выключен когда сработает import.
— 100% хорошо работает react-imported-component, так как оборачивает обображаемый компонент в AppContainer, и это «спасает» ситуацию, но его SSR немного специфичен, и подойдет не всем.
5. Я думаю это далеко не конец.
React-hot-loader это не серебрянная пуля —, а достаточно узкоспециализированный инструмент, который может быть очень полезен. А может и не быть.
Время попробовать?
Прямо сейчас мы близки к выпуску RC версии. Банально больше нет багов чинить и фич добавлять. Только немного code coverage повышать.
Сейчас самое то время установить RHL себе, и попробовать его в деле. И сообщить о всех проблемах что возникнуть (конечно же!) не должны : D
В общем — огонь!
github.com/gaearon/react-hot-loader/tree/next
npm install react-hot-loader@next