
Привет, Хабр! На связи снова Саша Мищенко, тимлид платформенной команды в Профи.ру. И сегодня я хочу поделиться нашей большой и, на мой взгляд, поучительной историей переезда с нативного кода на React Native.
Если кратко, то было интересно и иногда даже страшно. Баги, сложности, неочевидные подводные камни… В общем, история получилась длинная, поехали.
Начало. Большая мечта
Всё началось с мечты компании — стать масштабным маркетплейсом с высокой узнаваемостью. Но это слишком абстрактно, поэтому мы сформулировали цель поконкретнее: вырасти в 10 раз.
Амбициозно, да? Ещё как. Вот и стали думать, как этой цели достичь. Придумали вариант: радикально повысить скорость разработки.
У компании было (и есть) два основных продукта для тестирования этой гипотезы:
Бэк-офис — веб- и мобильные приложения для специалистов платформы (техников, электриков, сантехников и т. д.). Для них главное — функциональность приложения, а не продвинутый интерфейс.
Клиентское приложение — для заказа этих самых услуг у специалистов. Здесь, наоборот, пользовательский опыт и дизайн решали. Если человеку не зайдёт анимация или навигация, он уйдёт к конкуренту и уже вряд ли вернётся.
Изначально оба этих продукта были написаны нативно: отдельно на 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)

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

godder_543
09.10.2025 10:40У вас богатый опыт написания RN приложения взаимодействующего с пользователями, может вы встречались с такой проблемой для TextInput - если в onChangeText модифицировать value перед установкой, например нужно разбить вводимое число на разряды, то оно сначала отобразится слитно, и только через 1 рендер оно будет отображаться форматировано с разделением по разрядам - это касается любой модификации текста на ходу - это приводит к некому "морганию" текста и отображению сначала без форматирования, затем через 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`**.

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 не обновлялся несколько лет, и даже на гитхабе уже в архиве.
alelam
Возможно я слегка туповат и слишком плохо знаю RN, но что подразумевается под формулировкой "прокачали разработчиков в смежных сферах" применительно к тем, кто нативные клиенты пилил? Старички со свифта и котлина на RN переехали, а отказавшиеся в категорию экс-сотрудников перешли?:)
Pumppeedd Автор
привет!
объясню, то имел в виду под тем, что все разрабочики за счет стека получились универсальными.
наши ребята из фронтов могут писать мобильные приложения, потому что там тоже реакт. а раз уж залезли - иногда залезают и в нативный код, прокачиваются
ребята из нативных разработчиков остались с нами не все. но, кто остался, обучился разработке на react. И теперь помогают в нативной части, если там есть проблемы, а в остальное время кодят веб и приложения
ребята из бэкенда тоже могут залезть в код, узнать, как что работает, некоторые даже пишут простые компоненты, потому что у нас везде typescript. Это происходит, если задача не сложная, а у разработчика есть желание развиваться в фулстак. И так же они могут это делать и на вебе, и на мобильных приложениях
получаем своего рода фулстаков из всех разработчиков, которые хотели бы смотреть за границы своей специализации