Мы перешли на шестую версии React-router. Это помогло нам решить несколько проблем, например, определение маршрутов в Switch рендерит точный маршрут, а не первое совпадение, а размер бандла уменьшился почти в 2 раза.  

В статье расскажем про опыт миграции и подготовительный этап, трудности, возникшие после миграции и ошибки, информации о которых нет в официальной документации, и пути их обхода. Также будет альтернативный способ перехода с помощью официального пакета.

Об авторе:

Андрей Новиков

Старший разработчик Альфа-Банк

Зачем мы перешли, или Что не так с пятым React-router?

Зачем переходить на шестую? Чтобы сидеть и разбираться? Нет, просто с пятой версией были проблемы, которые решены в шестой.

#1. При определение маршрутов Switch рендерилось первое совпадение, а не точный маршрут. Чтобы React-router рендерил точный маршрут, как он забит в адресной строке, нужно было либо указывать свойство exact, либо соблюдать точный порядок для вывода нужных компонент. 

Например, на левой части картинки мы видим, что если захотим отрендерить, отобразить любой другой компонент с пользователями, например, страницы about или конкретным пользователем, то у нас всегда будет отображаться домашняя страница. Как исправить указано на правой части — переопределить порядок маршрутов или указывать везде свойство exact, что тоже трудоемкий способ. 

#2. Метод History.push() в пятой версии перестал запускать навигацию: ряд issue в репозитории GitHub есть до сих пор. 

#3. Push и replace сохраняли значения предыдущих search и hash строк. Например, если в адресной строке у нас есть id, то при использовании этих методов мы получим bar с id равным какому-то значению, вместо просто bar. Хотя ожидали, что путь обновится без лишних параметров. По этой проблеме также есть issue, в котором описано, как победить эту проблему, но там также указано, что скорее всего это баг, а не фича библиотеки.

#4. Routes запрашивает пути маршрутов с похожими, а не точными именами. Чтобы решить проблему, мы указывали свойство exact и тогда поведение было корректно. 

#5. Долгая сборка и развертывание. Пятая версии до сих пор разрабатывается — недавно вышла версия 5.3.2, в которой появилась поддержка React 18. Это всё сказывалось на размере: сборка и развертывание на клиенте выходила долгой.

Апдейт: Забегая вперед, отмечу, что после перехода, шестая версия у нас весила как 62% предыдущего, а скорость загрузки в несжатом виде на 74 мс меньше.

Bundle size

Bundle size

29 КБ, 9,5 КБ

18 КБ, 5,9 КБ

Download time

Download time

191 мс, 11 мс

117 мс, 7 мс

В какой-то момент мы с командой поняли, что уже не можем терпеть постоянно повторяющиеся ошибки пятой версии, каждый раз натыкаясь на одни и те же грабли. Терпение закончилось и мы решили переходить на шестую.

Этапы перехода: подготовительный и миграция

Проанализировав все эти проблемы, наша команда приняла решение, что пора попробовать новую версию и посмотреть, как всё работает под капотом. Мы разбили наш переход на 2 этапа: подготовительный и, непосредственно, сама миграция.

В подготовительный входит 4 шага. 

#1. Обновление React до 16.8 и выше, потому что теперь мы используем хуки, а в шестой версии уже убрали всю поддержку старого кода. 

#2. Обновление стиля передачи компонента маршрута в children вместо component. Изменился стиль передачи компонента в маршрутах: мы не передаем компоненты в props component или render props. Мы теперь должны передавать в children или как дочерний маршрут. Это облегчит переход на шестой Router, потому что синтаксис будет более похож, в отличии от предыдущей версии. 

На картинке пример. Нам было нужно немного поменять стиль, и если раньше мы всегда использовали привычный нам проп компонент (слева), то теперь мы используем children, либо вложенные маршруты. 

#3. Удаление <Redirect> в компоненте <Switch> на клиенте. На этом этапе меняем редирект: ставим предыдущий путь в Route path=’’about’’ и уже перенаправляем в пропе render. 

#4. Замена <PrivateRoute> на <Route render>. Чтобы приватные маршруты не отображались, когда это не нужно, <PrivateRoute> будет заменен на отдельный компонент, в котором и будет проводиться обработка авторизации. Мы используем пропс element и меняющуюся логику. 

В этап миграции входит 5 шагов. Говоря о шестой версии, мы должны изменить пути маршрутов на относительные: то, что мы выносили в дочерние маршруты, выносим в element.

#1. Обновление React-router до 6.2.0+.

#2. Обновление элементов <Switch> на <Routes>.

#3. Относительные маршруты и ссылки. Есть такой способ авторизации на клиенте, когда логику убираем в отдельные функции компоненты. На картинке пример из шестой версии, но нужно понимать отличия от предыдущего, подготовительного, этапа. 

Отличия в том, что вместо функции в пропе render используется element. Дочерние маршруты, что уже поменяли на предыдущем этапе, просто перемещаем в проп element. 

У нас появилась крутая возможность больше не матчить наши маршруты каждый раз, а задавать относительные пути. Дочерние компоненты смотрят на родительские. Если у родительских есть какой-то путь, то все дочерние будут отображаться в адресной строке относительно предыдущего. 

#4. Замена библиотеки react-router-config на хук useRoutes. Для тех проектов, где используется библиотека для объектной нотации, нужно перейти на хук useRoutes, который позволяет также хранить все маршруты в одном месте. 

У нас есть приложения, которые использовали дополнительную библиотеку — это react-router-config, позволяла использовать объектную нотацию. Этот способ полезен, когда у нас где-то есть один файл конфигурации, и мы можем даже не использовать JSX- разметку.

#5. useNavigate вместо useHistory. Это самая крутая фича — позволила нам уйти от тех проблем, которые упомянул в начале.

Теперь:

  • у нас корректно работает push по умолчанию;

  • если нам нужно использовать другие методы — они передаются вторым параметром, например, просто ставим replace со значением true;

  • если если что-то нужно добавить в стэйт, это также возможно.

Миграция завершена, но на этом всё не закончилось

На этом наша миграция закончилась и всё прекрасно работало, но как же без трудностей? 

На этапе тестирования мы посмотрели наши логи и обнаружили, что сыпятся ошибки о том, что React-router не может отобразить ни один компонент, хотя он прекрасно всё рендерил. 

Это было связано с тем, что на наших стендах приложение развёртывалось не в руте, а по определенному контекстному пути (context route), который у нас передается в basename.

Примечание. У нас микрофронтенды. Больше о микрофронтендах можно узнать из нашей статьи Webpack Module Federation: «официальное» решение в микрофронтендах.

Этот Conteхt Route при серверном рендеринге задавался для StaticRouter в проп basename. Оказалось, что указание этого пропа было излишним, в документации информации об этом нет, но при этом ошибка возникает.

Следующая ошибка возникла при переходе на React 18 — мы получали ошибки в консоль при продакшн сборке. Чтобы решить проблемы достаточно было не указывать basename в статик роутере. Приложение корректно работало как с серверным рендерингом, так и без него. Сообщения об ошибках перестали отображаться. 

Примечание. Но на клиенте baseName мы оставляем. 

Примечание. Если вы уже перешли на эту версию, напишите, возникала ли у вас подобная ошибка.

Минуса нашего подхода в том, что мы делали это всё за раз — это отнимало время на тщательное изучение и тестирование. Но такой способ мы выбрали потому, что раньше мы мигрировали на версию 6.2 и никаких других способов больше не было.

Есть альтернативный способ перехода. Его особенность в том, что команде не нужно за одну итерацию менять всё приложение, а достаточно поэтапно делать изменения, продолжая выпускаться, не останавливая релизный цикл и не блокируя выпуск бизнесовых задач. Этот пакет называется CompatRouter. Появился он с версией 6.3 React-router. 

Инкрементная миграция

Как проходит инкрементная миграция по шагам:

  • Установка CompatRouter.

  • Замена <Route> на <CompatRoute> в <Switch>.

  • Замена кода компонента, используя API v6 вместо v5.

  • Преобразование <Switch> в <Routes> и <CompatRoute> в <Route>.

  • Прохождение по компонентам дерева.

  • Удаление пакета совместимости.

Теперь подробнее.

Установка CompatRouter. С версии 6.3 на помощь пришел официальный пакет CompatRouter, который позволяет не делать всё сразу скопом, а переходить поэтапно, оставляя каждое изменение на коммиты. 

Во-первых, устанавливаем библиотеку. Установка простая: выполняем команду npm install, импортируем библиотеку и добавляем её перед Switch.

Меняем <Route> на <CompatRoute> в <Switch>. Зачем? Это некая прослойка — позволит использовать одновременно пятую и шестую версии.

В компоненте реализована такая логика:

  • она смотрит какие проп ей передаются — пятой версии или шестой;

  • сопоставляет;

  • применяет логику версии, например, для пятой логику пятой версии;

Замена кода компонента, используя API v6 вместо v5. Изменяем матчинг на хук useParams, чтобы предоставить необходимые параметры компонента. useHistory меняем на уже знакомый хук useNavigate. Если где-то использовался location props, мы меняем его на useLocation, который также позволяет нам достучаться до нужных свойств локации. 

Проходимся по ссылкам и меняем их на относительные. Обновляем абсолютные ссылки, основанные на матчинге, и меняем ссылки на относительные.

Важное изменение — exact в ссылках меняется на end. Вместо пропа для активного класса мы используем свойство isActive, и, уже исходя из этого, рендерим нужный нам стиль. 

Активный класс (activeClassName) и стиль (activeStyle) работают по умолчанию для NavLink. Если нужно передать другое имя для активного класса, тогда нужно передать функцию для свойства className. В эту функцию автоматически передается объект, у которого есть ключ isActive, который как раз проверяет активная ссылка или нет. То же самое можно сделать через свойство:

style:
 style={
        color: isActive ? 'var(--color-active)' : 'white',
   }

Преобразование <Switch> в <Routes> и <CompatRoute> в <Route>. Поскольку все дочерние компоненты у нас уже заменены на шестую версию и используется такая же версия АПИ, то необходимость в <CompatRoute> тоже исчезает.

Также заменяем всё не в ссылках, а в маршруте, на относительные ссылки. Компоненты передаем вместо component пропсом в элемент. 

Прохождение по компонентам дерева. Проверяем, что нигде не забыли заменить пути на относительные в ссылках, и включили ли глубокое сопоставление путей для компонент через символ “*”. 

Удаление пакета совместимости. На этом всё, наш переход закончен — мы просто удаляем вспомогательную библиотеку, она больше не требуется.

Примечание. Схема работает при обновлении и с более ранних версий, например, с четвертой или третьей. Но здесь придется намного дольше изучать документацию, потому что накопилось много изменений.

Преимущества и выводы

Мы можем визуализировать иерархию маршрутов с помощью хука useRoutes. Например, можем использовать компонент outlet для вывода этих компонентов в нужном месте.

Теперь порядок маршрутов определений не зависит от того, что будет отрендерено на странице. Мы не загружены лишними реквизитами и гибки в разделении маршрутов между компонентами. 

Шестая версия гибка в разделении маршрутов между компонентами и ранжирует маршруты, не загромождая реквизитами компоненты маршрута. 

Появление относительных маршрутов и ссылок, реализация полностью на хуках. Процесс перехода занимает мало времени, но при этом код шестой версии намного компактнее. Например, отправка аналитики теперь занимает 10 строк вместо 37.

Вес наших приложений уменьшился, а выпуск — немного ускорился. Мы больше не используем дополнительные библиотеки — всё работает из коробки, что здорово.

Процесс перехода у нас занял не так много времени, но стоит обратить внимание, что новый АПИ требует изучения, что увеличивает время вхождения новых разработчиков. От ошибок в новой версии тоже никто не застрахован.


Также рекомендуем почитать.

​​Подписывайтесь на Телеграм-канал Alfa Digital Jobs — там мы рассказываем про нашу работу (иногда шутки шутим), делимся новостями и полезными советами. Ещё есть Alfa Digital в ВК: постим новости, видео с митапов и недавно выпустили новое шоу «Из бэклога» совместно с Selectel и Space307 про удалёнку, собеседования, трекинг задач, взаимодействие команд, адаптацию новичков, горизонты планирования и конец Slack’а.

Комментарии (7)


  1. MrCheater
    07.09.2022 21:00
    +1

    Расскажите, пожалуйста, как вы такие приятные скрины кода сделали? Хочу так же


  1. chemaxa
    08.09.2022 00:44

    Расскажите как у вас безопасники отнеслись к пакетам которые вышли после 24.02?

    Просто есть компании в которых по правилам безопасности запретили ставить версии пакетов вышедшие после 24.02. А вы из банка, у вас возможно есть похожие требования.

    А по теме миграции, я лично предпочитаю просто делать реэкспорт всех библиотечных зависимостей из своего файла, тогда можно легко написать необходимую обёртку в случае необходимости и легко мигрировать.


  1. jakobz
    08.09.2022 09:35

    React Router - эталон как не надо вести проекты про библиотеки. 6-я версия уже, в каждой меняют api и всё ломают, без какого-то особого смысла.


  1. Tsimur_S
    08.09.2022 09:40

    Ого уже шестой есть оказывается. Помнится еще не так давно ломали копья какой лучше взять в проект: третий или четвертый.


  1. mSnus
    08.09.2022 09:48

    29 КБ, -> 9,5 КБ
    18 КБ -> 5,9 КБ
    шестая версия на 62% меньше

    А что из этого "на 62% меньше"? или это что-то из рекламы?


    1. deamondz
      08.09.2022 12:05

      разрабы реакт роутера схитрили немного, они выкинули систему регэкспов из путей, и за счёт этого (в том числе, и по большей части) сам пакет стал меньше. Зато теперь эта работа пересела на пользователей этой библиотеки (я согласен, что не все пользуются этой фичей, но мне лично стало неудобнее)


      1. mSnus
        08.09.2022 21:22

        да, но 18Кб - это "62% от 29Кб", а не "на 62% меньше", сократили на 38%.

        На 62% меньше - это если бы сократили с 29Кб до 9Кб, почти на 2/3.