Контекст задачи
Команда разделила единое приложение React Native, обслуживающее две роли пользователей (клиент и поставщик услуг), на два независимых приложения с общей библиотекой кода. Оба продукта уже опубликованы в Google Play и App Store. Стек проекта включал React Native 0.84, React 19, npm и bare workflow без Expo. Попытки следовать руководствам по monorepo обычно ограничиваются настройкой watchFolders — этого достаточно для сборки бандла, но не гарантирует корректную работу во время выполнения.
Критическая проблема со входом через общий экран авторизации
Самым затратным по времени оказался баг: при входе пользователя API возвращал статус 200, токен успешно записывался в хранилище, однако интерфейс оставался на экране логина. При этом ни ошибок, ни предупреждений в консоли не появлялось, а сетевые запросы выглядели штатно. Причина крылась в двух экземплярах библиотеки zustand из-за особенностей разрешения модулей Metro. Общие компоненты обращались к одному инстансу store, а оболочка приложения — к другому. Обе точки импорта разрешались успешно, поэтому среда исполнения не сигнализировала о проблеме. В результате запись состояния происходила в один магазин, а чтение — из другого, что делало симптом «залипания» на экране входа полностью объяснимым задним числом, но трудноуловимым в момент отладки.
Четыре дополнительных проявления двойных копий зависимостей
- Дубликаты React приводили к падениям вида Cannot read property 'current' of null внутри общих компонентов, которые сами по себе были написаны корректно.
- Два экземпляра axios ломали интерсепторы: состояние инстанса терялось, и заголовки аутентификации переставали автоматически добавляться к запросам из общего кода.
- Повторная регистрация нативных модулей затрагивала AsyncStorage, геолокацию и векторные иконки; симптомы варьировались и никогда прямо не указывали на истинную причину.
- Успешный tsc давал ложное чувство безопасности: TypeScript видел идентичные типы у разных экземпляров модуля и подтверждал их совместимость, хотя рантайм был сломан.
Решение через resolveRequest в Metro
Рабочий подход заключается в переопределении механизма поиска модулей через функцию resolveRequest в metro.config.js каждого целевого приложения. Суть метода — принудительно привязать все зависимости, импортируемые общим пакетом, к тем копиям, которые установлены непосредственно в приложении-потребителе. Список таких пакетов короткий и конечный; его лучше формировать автоматизированно, например grep’ом по кодовой базе общего пакета, чтобы ничего не упустить из виду. Дополнительно общая библиотека намеренно не объявляет runtime-зависимости, полагаясь на peerDependencies потребителей, что снижает риск дублирования транзитивных деревьев.
Практические рекомендации для подобных миграций
При выделении shared-пакета критично контролировать единообразие версий ключевых библиотек между хостами и ядром. Не доверяйте только успешной типизации: проверяйте реальное количество загруженных экземпляров чувствительных пакетов вроде React, менеджеров состояний и HTTP-клиентов. Настройте централизованное разрешение модулей до первого запуска, зафиксируйте список «опасных» зависимостей и автоматизируйте аудит дерева node_modules. Такой превентивный контроль существенно сокращает цикл обнаружения невидимых симптомов, когда внешне всё собирается, сеть отвечает 200, а пользовательский сценарий блокируется без единой строки ошибки.