Привет, Хабр! На связи снова Саша Мищенко, тимлид платформенной команды в Профи.ру. И сегодня я хочу поделиться нашей большой и, на мой взгляд, поучительной историей переезда с нативного кода на React Native.

Если кратко, то было интересно и иногда даже страшно. Баги, сложности, неочевидные подводные камни… В общем, история получилась длинная, поехали.

Начало. Большая мечта

Всё началось с мечты компании — стать масштабным маркетплейсом с высокой узнаваемостью. Но это слишком абстрактно, поэтому мы сформулировали цель поконкретнее: вырасти в 10 раз.

Амбициозно, да? Ещё как. Вот и стали думать, как этой цели достичь. Придумали вариант: радикально повысить скорость разработки.

У компании было (и есть) два основных продукта для тестирования этой гипотезы:

  1. Бэк-офис — веб- и мобильные приложения для специалистов платформы (техников, электриков, сантехников и т. д.). Для них главное — функциональность приложения, а не продвинутый интерфейс. 

  2. Клиентское приложение — для заказа этих самых услуг у специалистов. Здесь, наоборот, пользовательский опыт и дизайн решали. Если человеку не зайдёт анимация или навигация, он уйдёт к конкуренту и уже вряд ли вернётся.

Изначально оба этих продукта были написаны нативно: отдельно на iOS, отдельно на Android. Про это время могли бы рассказать старички или экс-сотрудники. Если будет интересно, созвонюсь с ними и расспрошу. 

И вот именно у них появилась идея перевезти бэк-офис-приложение на React Native.

Почему выбрали именно его? 

  • Единый стек: у нас бэкенд активно переезжал на Node.js, а фронт всегда был на реакте. И тогда мы подумали, что будет круто, если вообще вся разработка будет на одном языке — на TypeScript: так намного легче управлять ресурсами для задач, проводить онбординг и растить горизонтальность разработчиков.

  • Удобная организация команд: вместо разделения по технологиям (на iOS, на Android и на веб) мы перешли к нескольким департаментам. Одни занимаются платформой, другие — продуктом. О них рассказывал в прошлой статье.

Часть 1. Бэк-офис. История успеха

Переезд начали с бэк-офиса. На этом этапе меня ещё не было в команде, поэтому рассказываю со слов коллег. 

В приложении для специалистов не было ни сложных анимаций, ни многоступенчатого интерфейса. Благодать для быстрой и безболезненной миграции! Команда из 2–3 энтузиастов буквально за несколько месяцев переписала нативное приложение на React Native.

И жили дальше. Команды действительно стали работать быстрее. Горизонтальная структура сработала — разработчики могли гибко переключаться между задачами. Ускорения достигли и решили двигаться дальше. И через какое-то время начали думать о миграции клиентского приложения…

Часть 2. Ожидание vs реальность

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

Бизнес-задача была такой: не просто сменить технологию и ускорить разработку, но и апгрейднуть продукт. Мы хотели сделать новое и красивое приложение с микроанимациями, плавными переходами, умной навигацией, чтобы пользователь получал удовольствие не только от услуг, но и от их выбора, потому что сейчас никто не станет юзать неудобный и тяжеловесный софт. Нужно было выходить на новый уровень.

Итак, с какими проблемами мы столкнулись на практике.

Первая и самая очевидная — производительность. 

Мы упёрлись в классическое узкое горлышко — мостик между нативом и RN. Он должен был бороться со злом, а не примкнуть к нему… Так мы получили лаги и подвисания сначала из-за сериализации данных, потом из-за десериализации и т. д. 

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

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

Это была не просто техническая помеха, а прямая угроза бизнес-метрикам. И нам, как команде, которая отвечает за успех миграции.

Вторая проблема была уже на уровне экосистемы. 

Комьюнити React Native небольшое и молодое. Мы это особенно прочувствовали, когда нужно было внедрить яндекс-карту. Нативная реализация у ребят очень крутая, а вот под RN библиотек нет. 

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

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

В итоге пришлось силами платформенной команды с нуля писать свои модули. Разработали собственную библиотеку для управления сплеш-скрином, которая смогла обеспечить бесшовную, идеально синхронизированную анимацию перехода из RN-экрана в нативный. 

Наконец, третья бесячая проблема — это бесконечная борьба с обновлениями.

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

Поэтому каждое обновление обязательное, а это недели или даже месяцы работы: нужно проверить все наши кастомные модули, переписать код под изменившиеся API, найти замену сломавшимся библиотекам и провести полное тестирование. 

А отказаться нельзя. Apple и Google ежегодно ужесточают требования к версиям SDK. Не обновишься — твоё приложение просто не пустят в стор. В общем, сложная и рискованная миграция стала для нас не выбором, а вынужденным шагом.

Да, звучит не очень. Но! Когда компания готовилась к миграции, она об этом знала, да и мы были готовы. Собственно, поэтому и создали платформенную команду — группу разрабов, которые могут и хотят брать на себя и технический долг, и архитектурные риски, и проблемы инфраструктуры. 

Думали ли мы ещё о чём-то, кроме RN? Конечно! Например, смотрели в сторону Flutter. У него всё сильно лучше с производительностью и с комьюнити.

Но тогда мы бы потеряли универсальность наших разработчиков. Пришлось бы доучивать старичков новому языку Dart или привлекать новичков. Этого в наших планах не было.

Поэтому отказались.


В общем, пожалели ли мы? Не-а.

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

Плюсы:

  • Прокачали разработчиков в смежных сферах.

  • Ускорили скорость разработки и доставки фич.

  • Сэкономили ресурсы. Дизайн-систему поддерживают два человека для всех платформ, тогда как для нативной поддержки потребовалось бы минимум вдвое больше.

Минусы:

  • Разработчики тратят часы на обновления RN, поиск библиотек и подбивку решений.

  • Есть риск выгорания. Иногда хочется просто писать код, а вместо этого борешься с лагами и багами. Это выматывает.

  • Нужно много и часто коммуницировать с дизайнерами и продуктологами. Часть фич приходится упрощать или вовсе убирать, если их нельзя внедрить в несколько шагов.

Вывод

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

А у вас, кстати, был опыт работы с React Native? С какими проблемами столкнулись? Будет интересно почитать и, может быть, даже взять на заметку.

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


  1. alelam
    09.10.2025 10:40

    Возможно я слегка туповат и слишком плохо знаю RN, но что подразумевается под формулировкой "прокачали разработчиков в смежных сферах" применительно к тем, кто нативные клиенты пилил? Старички со свифта и котлина на RN переехали, а отказавшиеся в категорию экс-сотрудников перешли?:)


    1. Pumppeedd Автор
      09.10.2025 10:40

      привет!

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

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

      ребята из бэкенда тоже могут залезть в код, узнать, как что работает, некоторые даже пишут простые компоненты, потому что у нас везде typescript. Это происходит, если задача не сложная, а у разработчика есть желание развиваться в фулстак. И так же они могут это делать и на вебе, и на мобильных приложениях

      получаем своего рода фулстаков из всех разработчиков, которые хотели бы смотреть за границы своей специализации


  1. little-brother
    09.10.2025 10:40

    Интересная статья, если очистить шелуху: пока вас не было все реализовывалось отлично, но с вашим появлением что-то пошло не так... :)


    1. Pumppeedd Автор
      09.10.2025 10:40

      самое главное — результат, а он у нас отличный :)


  1. godder_543
    09.10.2025 10:40

    У вас богатый опыт написания RN приложения взаимодействующего с пользователями, может вы встречались с такой проблемой для TextInput - если в onChangeText модифицировать value перед установкой, например нужно разбить вводимое число на разряды, то оно сначала отобразится слитно, и только через 1 рендер оно будет отображаться форматировано с разделением по разрядам - это касается любой модификации текста на ходу - это приводит к некому "морганию" текста и отображению сначала без форматирования, затем через 1 рендер с форматированием. Я перерыл весь интернет, но не смог найти этой проблеме нормальное решение. Сможете пожалуйста поделиться опытом, может у вы встречались/решали подобную проблему?


    1. Pumppeedd Автор
      09.10.2025 10:40

      привет! завтра отпишусь и постараюсь ответить на вопрос) забрал


      1. godder_543
        09.10.2025 10:40

        спасибо большое, ожидаю ответа от вас )


    1. Kwentin3
      09.10.2025 10:40

      Это "моргание" или "скачок" текста — классический результат асинхронной природы React и того, как `TextInput` управляет своим состоянием.

      Краткий ответ: Чтобы это исправить, вы должны использовать `TextInput` как полностью контролируемый компонент, обязательно передавая ему проп `value`.

      Проблема возникает из-за гонки между нативным обновлением UI и циклом обновления React (JavaScript).

      1. **Нажатие клавиши:** Пользователь вводит символ (например, '0' после '100').

      2. **Нативное обновление (моментально):** Нативный `TextInput` (в iOS или Android) немедленно обновляет свой собственный внутренний текст. На экране на долю секунды появляется **"1000"**.

      3. **Событие `onChangeText`:** Сразу после этого срабатывает событие `onChangeText`, которое отправляет новое значение ("1000") через "мост" в ваш JavaScript-код.

      4. **Ваша логика форматирования:** Ваш код в `onChangeText` получает "1000", форматирует его в "1 000" и вызывает `setState("1 000")`.

      5. **Рендер React:** React запускает процесс перерисовки компонента.

      6. **Обновление пропа `value`:** Ваш компонент перерисовывается, и `TextInput` получает новый проп `value` со значением "1 000". Он принудительно обновляет нативный элемент, чтобы он соответствовал состоянию из React.

      7. **Визуальный "скачок":** На экране текст меняется с "1000" на "1 000". Этот быстрый переход и есть то самое "моргание", которое вы видите.

      Проблема в том, что вы на мгновение видите нативное, неконтролируемое состояние (`"1000"`) до того, как React успевает применить ваше отформатированное, контролируемое состояние (`"1 000"`).

      Как исправить?

      Решение — сделать компонент **строго контролируемым**. Это означает, что `TextInput` никогда не должен полагаться на свое внутреннее состояние, а всегда должен отображать только то, что ему передано в пропе `value` из вашего стейта в React.

      #### Пример правильной реализации:

      ```javascript

      import React, { useState } from 'react';

      import { SafeAreaView, TextInput, Text, StyleSheet } from 'react-native';

      // Функция для форматирования числа с пробелами

      const formatNumber = (value) => {

      // Убираем все нецифровые символы

      const cleaned = value.replace(/[^\d]/g, '');

      // Разбиваем на разряды

      return cleaned.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');

      };

      const App = () => {

      const [inputValue, setInputValue] = useState('');

      const handleTextChange = (text) => {

      // 1. Форматируем введенный текст

      const formattedText = formatNumber(text);

      // 2. Устанавливаем отформатированное значение в стейт

      setInputValue(formattedText);

      };

      return (

      <SafeAreaView style={styles.container}>

      <Text>Введите сумму:</Text>

      <TextInput

      style={styles.input}

      keyboardType="numeric"

      // Ключевой момент: мы привязываем значение инпута

      // НАПРЯМУЮ к нашему отформатированному состоянию.

      value={inputValue}

      onChangeText={handleTextChange}

      placeholder="Например, 1000000"

      />

      </SafeAreaView>

      );

      };

      const styles = StyleSheet.create({

      container: {

      flex: 1,

      justifyContent: 'center',

      padding: 20,

      },

      input: {

      height: 40,

      borderColor: 'gray',

      borderWidth: 1,

      marginTop: 10,

      paddingHorizontal: 10,

      },

      });

      export default App;

      ```

      Что здесь важно:

      1. **`value={inputValue}`**: Эта строка — ключ к решению. Она говорит `TextInput`: "Всегда отображай только то, что находится в переменной `inputValue`, и ничего другого". Это предотвращает первоначальное отображение неформатированного текста. `TextInput` будет ждать, пока React даст ему новое значение, и только потом обновится.

      2. **`onChangeText={handleTextChange}`**: Здесь мы перехватываем ввод пользователя.

      3. **Логика в `handleTextChange`**: Мы берем "сырой" ввод, форматируем его и обновляем *наше* состояние (`inputValue`). После обновления состояния React перерисовывает компонент, передавая новое, уже отформатированное значение в проп `value`.

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

      Для сложных случаев (маски ввода)

      Если вам нужна сложная логика форматирования (например, маска для телефона `+7 (999) 123-45-67` или для карт), лучше использовать готовые библиотеки. Они уже решают множество пограничных проблем, таких как положение курсора при редактировании в середине строки.

      Хороший пример такой библиотеки: **`react-native-text-input-mask`**.


      1. godder_543
        09.10.2025 10:40

        Нейронки естественно я тоже безустанно мучал, и chatGPT, и DeepSeek, и Qwen, и Grok. Приведенное решение не работает, можно проверить это на практике для убедительности. Я даже создавал вопрос на stackOverflow, с еще одним примером как это можно увидеть в реале (ответов там так и не появилось почти за год): https://stackoverflow.com/questions/79253174/textinput-blinking-flickering-when-i-format-text-in-onchangetext-react-nativ

        P.S. react-native-text-input-mask не обновлялся несколько лет, и даже на гитхабе уже в архиве.