услуга: Мобильная разработка
React Native без легаси: как мы переехали на Expo 54 и отказались от Redux
Какая была проблема
Наше мобильное приложение разрабатывалось около двух лет как bare React Native приложение. Оно росло силами разных разработчиков и обрастало кастомными нативными модулями.
К концу 2025 года это вылилось в конкретную боль:
- Любая мелкая правка могла упереться в конфликт нативных зависимостей в CocoaPods или Gradle-ошибку сборки на Android 12.
- Redux Toolkit в связке с FSD превратился в генератор бойлерплейта: чтобы добавить одно поле, нужно было создать слайс, экшены, thunk, селекторы и зарегистрировать редьюсер в корне.
- Навигация была «матрёшкой» из навигаторов, размазанной по отдельному слою — разобраться, как пользователь попадает из чата в детали задачи, было тяжело.
- Онбординг нового разработчика занимал около трёх дней: ручной pod install, настройка Gradle, объяснение внутреннего устройства навигации и стейта.
Зачем мы на это пошли?
Чинить проект «поверх» точечными патчами было дороже и рискованнее, чем пересобрать фундамент. Инкрементальный апгрейд грозил утонуть в ручном разрешении конфликтов в project.pbxproj и build.gradle. Поэтому мы выбрали путь greenfield: новый проект с нуля, перенос в него JS/TS-кода и постепенное вырезание легаси.
Цели были простые:
- Избавиться от боли с нативной сборкой и вернуть контроль над платформенным кодом.
- Убрать бойлерплейт Redux и разделить клиентское и серверное состояние.
- Сделать архитектуру понятной для любого React-разработчика с рынка, а не только для тех, кто рос вместе с проектом.
Работа заняла пять месяцев и затронула 1545 файлов: добавлено 16 700 строк, удалено 33 510, чистый результат — минус 16 810 строк кода.
Платформа и сборка
Раньше нативные папки ios/ и android/ жили прямо в Git, и любое добавление SDK означало ручные правки в AppDelegate.mm и build.gradle. Версии компиляторов плавали от машины к машине — то Kotlin Gradle Plugin конфликтовал, то iOS deployment target не совпадал с ожиданиями библиотеки. В такой обстановке онбординг нового разработчика растягивался на добрые три дня: pod install, ручная настройка Gradle, попытки понять, почему на его машине не собирается то, что собиралось у всех остальных.
Мы перевели управление нативной частью на декларативные рельсы. Нативные папки теперь генерируются командой prebuild и лежат в .gitignore, а вся конфигурация SDK, разрешений, иконок и bundle ID — в одном файле app.json. Настройка внешних SDK, вроде Firebase, оформляется Config Plugins вместо ручных вставок в нативный код, а версии компиляторов фиксируются декларативно через expo-build-properties. В итоге онбординг значительно сократился: клонировал репозиторий, запустил npm run dev — и работаешь.

Навигация
Навигация раньше держалась на пяти разных пакетах React Navigation и была императивной «матрёшкой» навигаторов — около 683 строк кода в отдельном слое, разобраться в которых с наскока было тяжело. Логика auth-гарда при этом размазывалась по разным навигаторам суммарно на 250 строк, и любое изменение в правилах доступа приходилось искать по всему стеку.
Мы заменили всё это на Expo Router — декларативный файловый роутинг, где структура экранов буквально повторяет структуру папок: защищённая и гостевая зоны разведены по каталогам (auth) и (unauthorized), а модальные окна вынесены в отдельный прозрачный роут. Прежние 683 строки превратились в лаконичные файлы-роуты, а вся логика auth-гарда — в компактный реактивный хук.

Стейт-менеджмент
На Redux Toolkit с redux-persist держалось и клиентское, и серверное состояние сразу. Чтобы добавить одно поле, нужно было создать слайс, экшены, thunk, селектор и зарегистрировать редьюсер в корне — а хранилище AsyncStorage работало через асинхронный JS-мост, из-за чего при обновлении данных по WebSocket приходилось либо перезапрашивать всё по HTTP, либо городить ручную инвалидацию.
Мы заменили эту связку на Zustand с Immer для лёгкого UI-стейта и React Query для серверных данных, а AsyncStorage — на react-native-mmkv с синхронным доступом к хранилищу. Добавить новое поле теперь — это одна функция в сторе, а события по WebSocket пишутся напрямую в кэш React Query, без лишних HTTP-запросов.
Второй эффект — мы разделили ответственность: Zustand хранит только лёгкий локальный UI-стейт, а кэшированием, пагинацией, инвалидацией и вебсокет-обновлениями серверных данных занимается React Query. При логауте кэш просто стирается вызовом queryClient.clear(), исключая утечку данных между аккаунтами.

Архитектура FSD
Раньше проект жил в семи слоях FSD, и отдельный слой processes отвечал только за навигационные стеки и авторизационные переключатели. Как только навигация переехала в файловую структуру Expo Router, а auth-гард — в корневой лейаут, этот слой полностью потерял смысл, и мы его удалили — архитектура сжалась до шести слоёв.

Вторая проблема, которую решили на уровне архитектуры — циклические импорты между мелко раздробленными фичами и сущностями. Мы сгруппировали код вокруг крупных доменов (например, «Задача», «Сообщение», «Регламент»), где каждый домен инкапсулирует свои API-запросы, локальный UI-стейт и вспомогательные компоненты. Фичи и виджеты теперь импортируют такие домены сверху вниз, а сами домены остаются независимыми друг от друга — циклы исчезли.

Заодно провели ревизию UI Kit: множество дублирующихся элементов заменили на единую систему базовых элементов, оптимизировав рендеринг тяжелых экранов.
Чтобы вычистить «файлы-призраки» и неиспользуемые экспорты, оставшиеся после переноса полутора тысяч файлов, подключили статический анализатор кода — он строит граф импортов от точек входа и находит мёртвый код, неиспользуемые типы и лишние npm-пакеты.
Нативные модули
Устаревшие сторонние библиотеки заменили на официальные модули экосистемы Expo (файловая система, пуши, тактильный отклик и др.), которые поддерживаются платформой из коробки.
Что получили на выходе
В цифрах:
- Удалено свыше 33 000 строк кода, добавлено 16 700 — чистый результат минус 16 810 строк.
- Затронуто 1545 файлов за пять месяцев работы.
- Архитектура сжалась с 7 слоёв FSD до 6.
- Множество дублирующихся UI-компонентов заменены на единую систему базовых элементов.
По сути:
- Быстрый онбординг. Не нужно днями объяснять новичкам устройство навигации и стейта — теперь это индустриальный стандарт (Expo, Zustand, React Query), знакомый любому React-разработчику.
- Предсказуемый стейт. Клиентские и серверные данные разделены: Zustand отвечает за лёгкие локальные настройки, React Query — за весь жизненный цикл серверных данных.
Главный итог не в количестве удалённых строк, а в том, что в проект больше не страшно заходить. Онбординг занял часы вместо дней а стейт стал предсказуемым. Впереди ещё много работы над функциональностью, но фундамент под неё теперь чистый и надёжный.
Читайте также:
- Экосистема «Вектор». Как мы заменили хаос на управляемый рост
-
Как UI-библиотеки ускоряют разработку и делают пользовательский интерфейс лучше
-
Чат-боты в MAX и Telegram: как они помогли автоматизировать контроль задач автошколы
-
Разработка веб-приложения для общепита. Coffee Anytime, Hot, Шампур
-
Как мы автоматизировали создание коммерческих предложений внутри 1С-Битрикс
Проконсультируем по всем вопросам