React Native по-прежнему часто описывают формулой «один код для iOS и Android». В 2026 году она звучит слишком просто. Современное приложение на React Native — это общий продуктовый слой на TypeScript, нативный интерфейс, Hermes, Fabric, JSI, фоновые процессы, platform-specific код и довольно много решений о том, где именно должна выполняться каждая задача.
Мы столкнулись с этим в Synchra.24 — мобильном приложении для линейных и распределённых команд. В проекте больше тысячи TS/TSX-файлов и свыше ста экранов, но рядом с ними живут Kotlin и Swift: геопозиция, push-уведомления, биометрия, системные виджеты и работа с медиа. Это не история о функциях нашего продукта и не попытка его рекламировать. Это разбор того, чем на практике стала разработка на React Native к 2026 году — и какую часть этой работы действительно можно поручить нейросетям.
Почему мы выбрали React Native
Когда мы выбирали основной стек мобильного приложения, задача не сводилась к тому, чтобы «сэкономить и написать один раз». Нам предстояло развивать довольно большой продукт одновременно для iOS и Android: с общей бизнес-логикой, множеством форм и экранов, частыми изменениями сценариев и интеграцией с возможностями телефона. Поддерживать две полностью независимые реализации означало бы не только писать больше кода, но и постоянно следить, чтобы они одинаково понимали права доступа, статусы, проверки и ответы сервера.
React Native позволял держать эту часть продукта общей. TypeScript уже был понятен команде, а React давал знакомую модель компонентов и состояния. Новый экран, изменение формы или правило отображения можно было реализовать один раз и сразу проверить на обеих платформах. Для растущего продукта это оказалось важнее обещания абстрактной «кроссплатформенности»: команда могла быстрее выпускать изменения и реже сталкивалась с тем, что iOS и Android незаметно разошлись по логике.
При этом нам не подходил стек, который закрывает доступ к нативной платформе. Было понятно, что часть функций потребует Kotlin, Swift, фоновых режимов и системных API. React Native оказался удобен именно тем, что не заставлял выбирать между общим кодом и нативными возможностями. Большую часть продукта можно оставить на TypeScript, а узкий platform-specific участок написать на языке самой платформы и подключить через понятный контракт.
Это не означает, что React Native объективно лучше нативной разработки или Flutter для любого проекта. Если приложение почти целиком состоит из глубокой интеграции с одной платформой, общий слой может только мешать. Если команда много лет работает с другим стеком, цена переобучения тоже изменит решение. В нашем случае сошлись три условия: большая общая продуктовая часть, необходимость выпускать iOS и Android синхронно и готовность команды обслуживать нативный код там, где он действительно нужен.
Выбор оказался не способом избавиться от платформенных различий, а способом держать их под контролем. Основные правила продукта живут в одном месте, а различия iOS и Android остаются видимыми и локальными. Всё остальное в этой статье — по сути, последствия этого решения.
New Architecture перестала быть экспериментом
Долгое время обсуждение React Native начиналось с Bridge: JavaScript отправляет сериализованные сообщения нативной стороне, та отвечает, а между ними копятся очереди и ограничения. В актуальной архитектуре центральную роль играет JSI — интерфейс, позволяющий JavaScript напрямую взаимодействовать с C++ и нативными объектами без прежней схемы сериализации через Bridge.
Поверх него работают несколько частей новой модели:
-
Fabric отвечает за новый рендерер и согласованную работу React с нативным деревом интерфейса;
-
TurboModules дают современный способ объявлять и загружать нативные модули;
-
Codegen генерирует связующий код из типизированной спецификации;
-
новый рендерер поддерживает возможности современного React: конкурентный рендеринг, transitions, automatic batching и синхронные layout-эффекты.
Это уже не опциональная экзотика. В React Native 0.84 удалили код Legacy Architecture, Hermes V1 сделали движком по умолчанию, а для iOS по умолчанию стали использовать предсобранные бинарники. В версии 0.86, актуальной на момент написания статьи, команда продолжила улучшать DevTools и платформенную интеграцию. Подробности хорошо описаны в официальных материалах о React Native 0.84, React Native 0.86 и New Architecture.
Но здесь есть важная оговорка: включённая New Architecture сама по себе не делает приложение быстрым. Она снимает часть старых архитектурных ограничений и даёт более подходящие инструменты. Длинная синхронная задача в JavaScript, сотни ненужных ререндеров или неудачная работа с изображениями никуда не исчезают.
Главный вопрос — не «на чём написано», а «где выполняется»
В небольшом приложении можно долго воспринимать React Native как обычный React с нативными компонентами. В сложном проекте полезнее мыслить потоками выполнения.
Упрощённая схема выглядит так:
JavaScript-поток хорошо подходит для состояния, маршрутизации, сетевых запросов и большей части бизнес-логики. Главный нативный поток рисует интерфейс и обрабатывает системные события. Для задач, чувствительных к каждому кадру, используются worklets, а события операционной системы и фоновые процессы проходят через нативные модули.
Хороший пример — интерактивная анимация, следующая за жестом пользователя. Если каждое смещение сначала ждать в занятом JavaScript-потоке, движение начинает отставать от пальца. Reanimated и Worklets позволяют выполнить эту короткую логику рядом с UI и обновить следующий кадр без такого ожидания. Но переносить туда всё подряд тоже не стоит: worklet — не универсальный второй backend, а инструмент для конкретного класса задач.
Нативный код не означает провал кроссплатформенности
Наличие Kotlin и Swift иногда воспринимают как доказательство, что React Native «не справился». На практике критерий другой: сколько продуктовых правил приходится поддерживать дважды.
В хорошем разделении общими остаются модели данных, навигация, серверное состояние, формы, валидация, дизайн-система и большая часть экранов. Нативный слой решает ограниченные задачи, для которых у платформ есть собственные API или жёсткие требования по производительности. Он должен иметь небольшой и понятный контракт.
Например, TypeScript-слою не обязательно знать, как именно iOS и Android получают событие от операционной системы. Ему достаточно общего результата вроде:
type SystemEvent = { type: 'notification-opened' | 'location-updated'; payload: Record<string, unknown>; occurredAt: number;};
Если обе реализации соблюдают этот контракт, продуктовая логика остаётся единой. Это и есть практическая кроссплатформенность: не отсутствие нативного кода, а отсутствие двух независимых приложений с постепенно расходящимся поведением.
Bare React Native и Expo — это выбор границы ответственности
Вокруг Expo до сих пор хватает устаревших представлений. Для множества приложений Expo сегодня — разумный выбор: он сокращает объём ручной настройки, упрощает сборку и предоставляет большой набор поддерживаемых модулей. Начинать новый проект сразу с bare workflow только ради ощущения «полного контроля» обычно нет смысла.
Bare React Native оправдан, когда команда действительно использует этот контроль. В Synchra.24 он нужен из-за фоновой геопозиции, системных виджетов, особенностей медиа и других platform-specific интеграций. Цена решения вполне материальна: Gradle, CocoaPods, Xcode build settings, Android Manifest, Info.plist, подписи, entitlement-файлы и совместимость SDK становятся частью повседневной разработки.
Поэтому вопрос лучше формулировать не как «Expo или настоящий React Native», а так: какой объём нативной инфраструктуры команда готова обслуживать сама? Если ответ — минимальный, управляемая экосистема будет преимуществом. Если приложение упирается в нестандартные возможности платформы, bare workflow даёт нужную свободу, но выставляет за неё счёт временем разработчиков.
Один интерфейс не бывает полностью одинаковым
Общий JSX не отменяет различий iOS и Android. На реальном устройстве расходятся клавиатура, safe areas, системная кнопка «назад», разрешения, фоновые режимы, жизненный цикл уведомлений, URI файлов и даже момент, когда экран считается видимым.
В 2026 году к этому добавляется обязательный edge-to-edge на современных версиях Android. React Native 0.86 отдельно закрепляет это поведение для Android 15 и новее. Экран, который аккуратно выглядит на iPhone, может неожиданно попасть под статус-бар или системную навигацию на Android. Исправлять это одним глобальным отступом — почти всегда путь к следующему дефекту.
Рабочая стратегия — сохранять общую структуру экрана и локализовать различия:
const verticalOffset = Platform.select({ ios: 0, android: 8, default: 0,});
Для больших различий уместны файлы .ios.tsx и .android.tsx, но развилка целого экрана должна быть последним вариантом. Чем крупнее платформенная копия, тем быстрее исправление на одной платформе забывают перенести на другую.
Особенно опасны функции, которые хорошо имитируются. Геопозиция может вернуть тестовые координаты, а push — показаться через локальный сценарий. Но разрешение после повторного отказа, восстановление приложения из background, холодный старт по уведомлению и энергосбережение проверяются только на реальном устройстве.
Клавиатура: когда мы перестали поднимать экран и вынесли ввод в модалку
Одной из самых неприятных проблем в нашем React Native-приложении оказалась обычная экранная клавиатура. Сценарий знаком многим: пользователь нажимает на поле в нижней части сложной формы, клавиатура появляется, и приложение должно оставить поле видимым. На простом экране это выглядит как задача для KeyboardAvoidingView. На экране с навигационным заголовком, safe area, вложенным скроллом, нижней панелью или bottom sheet начинается борьба нескольких систем за одну и ту же высоту.
Сам KeyboardAvoidingView не «находит поле и аккуратно прокручивает к нему». Он меняет высоту, позицию либо нижний padding своего контейнера в зависимости от размера клавиатуры. Даже официальная документация отдельно предупреждает, что Android и iOS взаимодействуют с его behavior по-разному.
На iOS у нас использовался вариант padding: к контейнеру добавлялось пространство высотой с клавиатуру. На Android приложение реагировало изменением высоты, а системное окно могло параллельно работать в режиме adjustResize. С translucent status bar, edge-to-edge, safe area и вложенными контейнерами итоговая геометрия зависела уже не от одного значения. Один слой уменьшал доступную высоту, второй добавлял отступ, скролл пытался показать сфокусированное поле — и экран мог подняться дважды, прыгнуть во время анимации либо вернуться не в исходную позицию после закрытия клавиатуры.
Особенно плохо воспроизводились пограничные случаи: многострочное поле, переключение между соседними полями, быстрое закрытие, жест «назад» на Android, повторное открытие и клавиатуры разных производителей. Одно значение keyboardVerticalOffset исправляло конкретный экран и ломало другой. Универсального «правильного отступа» не существовало, потому что верхняя граница контента отличалась от маршрута к маршруту.
При этом даже «Android» оказался слишком широким понятием. Один и тот же TextInput с одинаковыми props мог нормально работать на Huawei и иначе — на Xiaomi. Мы встречали некорректное автозаполнение и особенно странный эффект, когда для ввода обычного пробела пользователю иногда приходилось нажимать пробел дважды.
Причина такого класса дефектов не обязательно находится в самом компоненте React Native. Между нажатием клавиши и onChangeText стоит IME — установленная системная клавиатура, её словарь, автокоррекция, composing text и сервис автозаполнения прошивки. Производители Android-устройств поставляют разные комбинации этих компонентов и по-разному меняют их поведение. Клавиатура может сначала держать редактируемое слово как незавершённую композицию, затем предложить замену и только потом зафиксировать пробел. Если контролируемое React-поле в этот момент получает value обратно из состояния, форматирует строку или размонтируется, IME способна воспринять обновление как отмену ещё не подтверждённого символа. Первое нажатие завершает композицию, и лишь второе действительно вставляет пробел.
Поэтому проверка только на одном Android-эмуляторе давала ложное чувство готовности. Для полей пришлось отдельно учитывать обычный ввод, автокоррекцию, автозаполнение, голосовой ввод, маски и secure-режим, а тестовую матрицу расширить хотя бы несколькими реальными устройствами и клавиатурами. Модальный редактор не унифицирует сторонние IME, но убирает из уравнения прыгающий сложный экран и оставляет гораздо меньше факторов, которые могут вмешаться в composing-сессию.
В итоге мы перестали пытаться каждый раз перестраивать исходный экран. Видимое поле формы стало нефокусируемым представлением значения. Нажатие на него открывает отдельную полноэкранную Modal, внутри которой находится настоящее редактируемое поле:
Модалка изолирует изменение геометрии. Клавиатура больше не двигает навигацию, карточки и кнопки основного экрана: ей противостоит простой контейнер flex: 1, а поле располагается в его центре. После завершения ввода значение передаётся тому же обработчику, который обслуживал поле на форме. Для пользователя это выглядит как сфокусированный режим редактирования, а для layout-системы — как новый экран без сложного окружения.
Даже закрытие потребовало аккуратной последовательности. Сначала поле теряет фокус, затем вызывается Keyboard.dismiss(), но сама модалка остаётся смонтированной до события keyboardDidHide. Иначе на Android окно могло начать восстанавливать высоту уже после удаления модалки, из-за чего на мгновение был виден сжатый или смещённый основной экран. На случай клавиатур, которые присылают событие с задержкой или не присылают его вовсе, оставлен запасной таймер: 250 мс для iOS и 600 мс для Android. Это не «магические значения» для расчёта layout, а только страховка после команды закрытия; нормальный путь завершается сразу по keyboardDidHide. Ограничения событий на Android также отмечены в документации модуля Keyboard.
Решение нельзя назвать универсальной рекомендацией. Для короткой формы с двумя полями стандартный KeyboardAvoidingView проще и естественнее. Модальный редактор добавляет переход, требует правильно сохранить автозаполнение, маски, secure-ввод, accessibility focus и черновое значение. Зато в большом приложении он дал нам важное свойство: поведение ввода перестало зависеть от структуры каждого отдельного экрана.
Этот случай хорошо описывает React Native-разработку вообще. Иногда проблема находится не в JavaScript и не в нативной платформе по отдельности, а в их взаимодействии. И лучший выход — не ещё один условный offset для iOS и Android, а изменение самого пользовательского сценария так, чтобы спорная часть layout стала проще.
Производительность: Fabric не отменяет профилирование
Hermes V1 и современный рендерер улучшают фундамент, но пользователь ощущает не архитектуру, а время до первого полезного экрана, плавность прокрутки и реакцию на нажатие.
Типовые источники проблем остались довольно земными: длинные задачи в JS, нестабильные props, слишком широкие подписки на состояние, большие изображения, невиртуализированные списки и каскады запросов. Появился и новый риск — чрезмерное увлечение синхронными возможностями JSI. Прямой вызов быстрее старого Bridge, но тяжёлая синхронная функция всё равно блокирует вызывающий поток.
В нашем стеке большие коллекции виртуализируются, а серверное состояние хранится отдельно от локального состояния интерфейса. Но ни FlashList, ни TanStack Query, ни memo не являются кнопками ускорения. Кэш с неправильной инвалидацией показывает старые данные, а мемоизация с постоянно меняющимися объектами только усложняет код.
Измерять стоит прежде всего release-сборку. Dev mode добавляет проверки и служебную работу, а отладчик меняет временные характеристики. Для JavaScript и React полезен React Native DevTools; для кадров, памяти, сети и нативных зависаний всё ещё нужны Android Studio и Xcode Instruments. Официальная документация прямо подчёркивает, что React Native DevTools не заменяет нативные инструменты.
Обновление React Native — это небольшая миграция платформы
Номер версии в package.json — самая простая часть обновления. Следом меняются шаблоны iOS и Android, требования к Node, Gradle, Kotlin, CocoaPods, Xcode и сторонним библиотекам. Пакет может заявлять поддержку New Architecture, но внутри обращаться к устаревшему API или предполагать другую версию React.
В нашем проекте есть локальные патчи для отдельных нативных библиотек, в том числе для фото-редактора и системных интеграций. Такой патч лучше ручной правки в node_modules, потому что воспроизводится после установки зависимостей. Но он всё равно является долгом. При каждом обновлении нужно проверить, не исправлена ли проблема upstream и совпадает ли патч с новым исходником.
Полезно вести не просто список пакетов, а матрицу совместимости: версия React Native, React, Reanimated, Worklets, библиотек уведомлений, минимальные iOS/Android SDK и используемый Xcode. Обновлять по одному слою и собирать обе платформы после каждого значимого шага обычно быстрее, чем потом разбирать десятки взаимосвязанных ошибок.
React Native 0.85 добавил возможность нескольких одновременных CDP-подключений к приложению: DevTools, IDE и AI-агент могут работать параллельно. Это заметное улучшение самого процесса разработки, но не отмена платформенного стека. Подробнее — в анонсе React Native 0.85.
Архитектура большого приложения начинается не с папок
Первичную архитектуру такого приложения, на мой взгляд, должен создавать человек. Нейросеть легко выдаст убедительную схему с features, entities, репозиториями и десятком абстракций. Но она не присутствовала на разговорах о продукте и не чувствует, какие решения для бизнеса действительно долгосрочные. Она не знает, какая функция через полгода должна заработать без сети, какой модуль придётся поддерживать одновременно в облачной и коробочной версиях, где особенно опасна потеря данных и какие части команда будет менять каждую неделю.
Именно в начале принимаются самые дорогие решения: где заканчивается экран и начинается бизнес-логика, кто владеет состоянием, как приложение общается с нативной платформой и что произойдёт при плохой сети, устаревшем сервере или закрытии приложения. Если эти границы сгенерировать без понимания продукта, получится архитектура, которая красиво выглядит на диаграмме, но мешает при первом нестандартном сценарии. Поэтому человек задаёт основные правила и осознанно принимает компромиссы. AI здесь полезен как собеседник: может найти противоречие, предложить альтернативу или проверить, не забыта ли одна из платформ. Но окончательное решение и ответственность за него остаются у команды.
Когда в приложении больше сотни экранов, порядок в папках ещё не означает порядок в коде. Можно аккуратно разложить файлы по screens, components, hooks и services, но оставить один экран ответственным буквально за всё: загрузку данных, проверку прав, открытие модалок, навигацию, отправку формы и обращение к системным функциям телефона. Такой экран сложно менять не потому, что он лежит не в той директории, а потому, что любое действие задевает несколько других.
Мы стараемся разделять код по простому вопросу: кто за что отвечает? Запрос получает данные от сервера. Отдельное преобразование приводит их к виду, с которым удобно работать приложению. Экран решает, что показать пользователю. Нативный модуль выполняет конкретную системную операцию — например, получает местоположение — и возвращает результат, но не решает, имеет ли сотрудник право продолжить сценарий.
Это разделение особенно помогает, когда сервер меняется со временем. В одном ответе идентификатор может прийти числом, в другом — строкой, а какое-то поле у старой коробочной установки вообще отсутствует. Если каждый компонент работает с таким ответом напрямую, проверки и преобразования быстро расползаются по всему приложению. Гораздо спокойнее привести данные к единому формату сразу после получения, а остальной интерфейс избавить от знания об этих различиях.
Похожая история с облачной и коробочной версиями. Мы используем одну мобильную сборку, но при запуске она получает адрес нужного API, файлового хранилища и других сервисов. Важно, чтобы конкретные домены не были случайно зашиты внутри экранов. Экрану загрузки файла должно быть всё равно, работает пользователь с облаком или с сервером своей компании: правильный адрес ему предоставляет общий слой приложения.
Конечно, идеально разделить всё с первого раза не получается. Один из наших экранных контроллеров со временем вырос примерно до трёх тысяч строк. В нём оказалось слишком много запросов, обработчиков и условий, и любое изменение требовало сначала заново понять половину файла. AI хорошо помогает в такой работе: находит повторяющиеся части, переносит функции и обновляет импорты. Но просто разрезать три тысячи строк на десять файлов недостаточно. Если все десять продолжают зависеть друг от друга и менять одно общее состояние, сложность никуда не исчезает — она только перестаёт помещаться на одном экране монитора. Решение о том, какие части действительно могут жить самостоятельно, остаётся архитектурной работой человека.
Где AI действительно помогает React Native-разработчику
Самая полезная роль AI в таком проекте — не генерация очередного компонента. Ценность появляется там, где человеку приходится одновременно удерживать TypeScript, Kotlin, Swift и конфигурацию сборки.
Например, при обновлении React Native агент может сопоставить package.json, Podfile, Gradle-конфигурацию, нативные регистрации модулей и локальные патчи. При разработке TurboModule — проверить симметрию Android- и iOS-реализаций относительно TypeScript-спецификации. При падении сборки — связать сообщение Codegen с конкретной несовместимой библиотекой, а не предлагать наугад очистить все кэши.
Хорошо работает и анализ платформенной симметрии: какие разрешения запрашиваются, что происходит при denied и blocked, одинаково ли останавливаются фоновые подписки, возвращают ли реализации один формат ошибки. Это скучная, распределённая по файлам работа, для которой машине проще собрать карту, а человеку — проверить вывод.
Но AI очень убедительно ошибается, когда читает проблему только по поверхности. В случае с клавиатурой очевидным ответом модели было бы подобрать keyboardVerticalOffset, добавить ещё один KeyboardAvoidingView или завернуть форму в очередной keyboard-aware scroll. Такой патч действительно способен исправить один экран на одном устройстве — и одновременно добавить двойной подъём на другом. Чтобы увидеть настоящую причину, пришлось проследить всю иерархию: глобальный контейнер приложения, safe area, навигацию, вложенный скролл, режим adjustResize, момент размонтирования поля и события конкретной Android-клавиатуры. Только после этого стало понятно, что надёжнее изменить сценарий ввода, а не искать очередное «правильное» число отступа. Это хороший предел AI-подхода: локальный код он исправляет быстро, но границу проблемы всё ещё нужно доказать наблюдением за системой целиком.
Для React Native особенно характерны ещё три вида ошибок AI:
-
смешение API Legacy и New Architecture в одном решении;
-
код, который компилируется, но нарушает lifecycle уведомлений или background task;
-
исправление только Android либо только iOS без проверки второго контракта.
Поэтому хороший запрос к AI начинается не со слов «почини ошибку», а с контекста: версия React Native, архитектура, целевые платформы, устройство или симулятор, debug или release, ожидаемый lifecycle и границы нативного модуля.
Практический цикл разработки с AI
У нас прижился довольно простой порядок. Сначала разработчик или агент строит карту пути: экран, состояние, API, нативный модуль, поток выполнения и платформенные файлы. Затем формулируется небольшое изменение с наблюдаемым результатом. После него запускаются статические проверки и сборки обеих платформ. Геопозиция, push и background-поведение обязательно проходят физическое устройство.
AI можно дать логи Metro, Gradle или Xcode, попросить сравнить реализации и предложить минимальный патч. Но финальным доказательством остаётся не уверенный ответ модели, а воспроизводимый сценарий: холодный старт, возврат из background, отказ в разрешении, плохая сеть, длинный список и release-сборка.
В этом подходе нейросеть не заменяет знание React Native. Напротив, чем лучше команда понимает JSI, потоки, lifecycle и ограничения платформ, тем точнее она ставит задачу машине и тем быстрее замечает правдоподобную ошибку.
Что в итоге представляет собой React Native в 2026 году
React Native больше не выглядит как веб-слой, приклеенный к двум платформам через медленный мост. New Architecture, Hermes и зрелая экосистема позволяют строить большие приложения с общей продуктовой логикой и настоящим нативным интерфейсом.
Но и обещание «написать один раз и забыть о платформах» не стало правдой. Успешная команда понимает, что должно жить в JavaScript, что — рядом с UI, а что — в Kotlin и Swift; измеряет release-сборки; планирует обновления как миграции; проверяет физические устройства и не маскирует нативные различия бесконечными условными отступами.
AI заметно ускоряет эту работу: читает большие связки файлов, помогает с миграциями, сравнивает платформы и разбирает логи. Но его польза максимальна не там, где ему отдают проект целиком, а там, где человек задаёт архитектурные границы и требует проверяемого результата.
Пожалуй, самая точная формула React Native сегодня звучит так: не один код для двух платформ, а одна продуктовая модель с осознанно выбранными нативными участками.
ссылка на оригинал статьи https://habr.com/ru/articles/1072522/