Facebook переписал большую часть React'а и выпустил 16 версию. React 16 был очень ожидаемым обновлением, особенно ввиду нового способа рендеринга Fiber, который сильно повышает производительность. Команда разработчиков React в последней версии усердно помечала методы и пакеты устаревшими (deprecated), и мы видели их предупреждения в консоли. В действительности же, миграция не так проста для большого проекта.
Мы в Discord только что запустили обновление нашего приложения на основе React 16 и хотим поделиться нашим опытом, который мы получили в ходе миграции.
Зачем мы это сделали?
Архитектура Fiber выглядит так, что она сможет сильно повысить плавность и скорость работы как рендеринга DOM-дерева, так и Native-компонентов путем разбиения процесса рендеринга на составляющие, давая предпочтение более быстрым частям. Прямо сейчас не очень ясно, являются ли эти преимущества существенными (например, асинхронный рендеринг все еще не включен), но эти изменения прокладывают путь в светлое будущее.
В конечном счете, вам придется обновиться. React находится в движении, и 16+ сообщество обязательно оставит вас брошенным наедине с багами. А в качестве бонуса React 16 приносит новые фичи, например, ErrorBoundary. В нем множество изменений, к которым вам придется привыкнуть.
Геморрой начинается здесь
Как и все миграции, уровень боли будет прямо коррелировать с давностью вашей кодовой базы. Facebook сделал хорошую работу в течение последних месяцев, помечая deprecated-методы ошибками, но не все библиотеки хорошо разработаны, и если вы зависите от таких библиотек или же от библиотек, которые вы форкнули, вам придется не сладко.
Также придется вашей команде решить пришло ли время полностью переходить на ES6-классы или на функциональные компоненты. Мы строго рекомендуем делать это после успешной миграции, так что вы сможете понять, что сломалось (а еще лучше, покрывая тестами) до того, как вы обновите проект.
Мы писали качественный код, но этого оказалось недостаточно, чтобы легко перейти с миксинов и React.createClass. Мы также использовали конкретные версии пакетов, что давало нам стабильность и уверенность, но наши зависимости стали старыми и покрылись мхом.
Вот здесь мы и столкнулись с болью от миграции.
Запуск codemod
Прежде всего вы должны использовать codemod, чтобы переехать с React.PropTypes на prop-types, и мигрировать с React.createClass. Codemod поможет в правильном выборе: использовать class Component где это возможно (или PureComponent в случае простого миксина), а где использовать create-react-class.
Установите jscodeshift, а склонируйте react-codemod репозиторий. Затем запустите следующие команды для вашего проекта:
1. Переход на prop-types
jscodeshift -t ./react-codemod/transforms/React-PropTypes-to-prop-types.js /path/to/your/repo
2. Переход на Component, PureComponent или createReactClass:
jscodeshift -t ./react-codemod/transforms/class.js --mixin-module-name=react-addons-pure-render-mixin --flow=true --pure-component=true --remove-runtime-proptypes=false /path/to/your/repo
Последнее — это последовать хорошо описанным советам в репозитории react-codemod. Наиболее важные из них:
- Переход на ES6-классы, если нет миксинов, и на PureComponent, в случае простых миксинов.
- Убедиться, что если в созданных классах присутствуют все static-параметры.
- Созданы аннотации из React.propTypes.
- Перенесены привязывание контекста
this.method.bind(this)
методов из конструкторов в стрелочные функции (если, конечно, это вам по душе).
- Пропущены все компоненты, которые используют устаревшее API, например,
isMounted()
метод, и так далее.
Codemod найдет много ошибок в проекте, которые должны быть записаны и затем исправлены вручную.
Если вам не понравится это автоизменение, вы можете поправить вручную что угодно на свой вкус. Codemod — это прекрасный инструмент, но не волшебная палочка. Он не мигрирует проект полностью за вас!
Поиск и замена
Мы обнаружили множество мест в коде, которые пропустил codemod. Судя по всему, там, где мы импортировали и вызывали PropTypes напрямую. Больше всего ручных исправлений пришлось на выискивание вызовов метода isMounted()
и создание свойства _isMounted, которая равнялась true
внутри componentDidMount
и false
внутри componentWillUnmount
(но, возможно, вам так делать не стоит)
Отказ от использования приватного API
Конечно же, вы не используете внутренние API фреймворка React, верно? Те, которые лежат в react/lib/*
. Если вы в своем проекте полагаетесь на них, то для вас плохие новости: вы за бортом.
Некоторые API были перемещены во внешние библиотеки (Семейство пакетов «react-addons-»), а некоторые, вовсе, были удалены.
В нашем проекте мы полагались на самописный TransitionGroup, которые зависел от flattenChildren-функции из недр react/lib
. Но теперь этой функции нет, как и childMapping и mergeCildMapping. Разработчики фреймворка в таком случае рекомендуют просто взять и скопировать эти недостающие функции. Это мы в итоге и сделали. Однако, мы встроили новые версии тех функций из пакета react-transition.
Таким образом, мы столкнулись с первой серьезной проблемой. Но наше приложение по прежнему не работало.
Обновление зависимостей
Этот пункт оказался одним из самых утомительных. И если вы не лихорадите обновлением до 16 версии, разумно будет подождать, пока большинство библиотек обновятся. Если вы хотите заняться обновлением, то это поможет каждому.
Консоль по прежнему показывает предупреждения о пакетах, которые используют React.PropTypes или React.createClass. Вам нужно будет обновить эти пакеты. Либо придется их заменить, либо как-то обойти, написав свое решение. Или же, форкнуть и поправить их, если пакеты кажутся заброшенными.
Основные проблемы, которые нужно решить:
- React.PropTypes
- React.createClass
- Зависимости от внутренностей фреймворка версии меньше 16
К сожалению, не все ошибки легко найти, и здесь кроется настоящий геморрой.
Столкнувшись с ошибкой, целых два дня ушло на поиск искомой библиотеки. И у вас будет тоже самое. Консоль продолжала говорить нам, что reactRiberChild не может зарендерить элемент, потому что атрибут ref
был undefined
. Мы рвали волосы на голове, выискивая что могло пойти не так, и в конечном счете копнули внутрь кода React, где увидели причину ошибки. Оказалось, что 16-й версии React не нравится ref: undefined
, а вот null
вполне себе ок. После этого стало понятно, что создаваемые элементы с помощью одной внешней библиотеки имели атрибут ref: undefined
. После того, как мы форкнули библиотеку, модифицировав ее код, все заработало прекрасно.
Особый геморрой
Что же, мы еще обнаружили и трудноуловимые ошибки. Опять же, в большинстве случаев требовался переход на новые версии библиотек или же их модификацию. Часто бывает, что при обновлении достаточно старой библиотеки, она меняет поведение элементов, и мы получаем полный швах в наших прекрасно написанных CSS-правилах.
Большинство библиотек только фиксировала свои проблемы, связанные с предупреждениями в React 15. И только самые последние их версии будут работать в React 16. Это утомительная работа по поиску и обнаружению ошибок.
Переход на ES6-классы означает, что вам теперь нужно осторожным и не менять атрибут props. Раньше было просто предупреждение в консоль об этом, теперь это падает с ошибкой “Cannot assign to read only property XXX of object ‘#’”. Если вы деструктурируете атрибут props, и пытаетесь изменить его свойства, эта ошибка вас ударит по рукам. Вместо этого используйте Object.assign или его синтаксический сахар:
const newProp = {...prop, propertyToOverride: overrideValue}
Также React 16 будет предупреждать о получении bolean-значения у onClick-метода. Это случится, если вы напишете
onClick={!disabled && this.handleOnClick}
Поэтому придется управлять поведением внутри назначенной функции.
И тут выходит на связь React Native
У нас ведь есть еще iOs приложение, которое делит stores, utils и некоторые action creators с веб-приложением. Переход на React 16 также смотивировал нас использовать последний релиз React Native, который зависит на альфа-версии React 16.
К сожалению, нам нужны специальны таймеры для видео, и у нас был форк React Native, и мы вернулись на версию 0.34. React 16 совместимость пришла в React Native только с версии 0.48.0, так что это время для обновления…
Это оказалось особенно болезненным. Наибольшей головной болью оказалось, что мы использовали React Native Webpack Server, который позволял повторно использовать код между нашими проектами. Так что мы пересмотрели взгляды на использование сборщика и стали переходить на Haul.
Haul держит нас по-прежнему во вселенной Webpack, но было не так просто его настроить на существующий проект. Рассказ об этом тянет на отдельную статью.
Мы раньше импортировали изображения для iOs с помощью webpack require(‘image!image_src’)
, но теперь нет возможности использовать его в новых версиях React Native. Это была изнурительная работа — нам требовалось перемещать изображения и менять способ доступа к ним. Даже после использования codemod, эта часть работы была проделана в основном вручную.
Также мы обнаружили множество устаревших классов, и сейчас мы советуем использовать костыль в виде react-native-deprecated-custom-components. Вам понадобится самая последняя версия, так как они недавно обновили пакет для React 16.
Берем в работу новые фичи
Самом лучшим нововведением для нас оказался новый ErrorBoundary. React 16 теперь прекращает рендер дерева в случае ошибки (что хорошо), и вы сможете отрендерить какое-то сообщение об ошибке. Дополнительно, ограничения дают хорошую возможность иметь отдельное место для логирования хорошо описанных ошибок с информацией о стеке вызовов и о компонентах.
Это запустилось!
Итак, теперь это работает, и пользовательский опыт стал полон восторга и удовлетворения? Нет. Мы обнаружили, что быстрее работать не стало. Но будет, так как новая архитектура дает нам новые возможности.
Самое большое преимущество при игре в долгую. Наш код был обновлен, что позволило нам нормализовать большую часть кода, пересматривая некоторые из основных инструментов. Это хорошо скажется на будущих модернизациях.
Производительность — это неплохой бонус.
Автор: Павел Щеголев