Рано или поздно в RN-проекте нужен нативный код: SDK банка, своя камера, датчик, фоновая работа. На middle и senior собесах просят рассказать, как связать JS и натив, и что изменилось с новой архитектурой.
Когда нужен свой модуль
Сначала ищут готовую библиотеку: экосистема большая, у Expo есть модули почти на всё. Свой модуль пишут, когда нужно встроить закрытый SDK, нужна производительность (обработка изображений, криптография) или платформенная функция, которой нет в библиотеках.
TurboModule: путь с Codegen
- Спецификация на TypeScript: интерфейс модуля.
// NativeBattery.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getLevel(): Promise<number>;
isLowPowerMode(): boolean; // синхронный метод
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeBattery');
- Codegen по спецификации генерирует интерфейсы на C++, Kotlin/Java и Objective-C++ и склейку с JSI. Конфигурация в
package.json(codegenConfig), генерация запускается при сборке. - Реализация на Kotlin и Objective-C/Swift: класс реализует сгенерированный интерфейс и регистрируется в пакете.
- Вызов из JS обычной функцией:
await NativeBattery.getLevel().
Что изменилось по сравнению со старыми модулями: модуль грузится лениво при первом обращении, вызов идёт через JSI без JSON-сериализации, типы JS и натива сверяются при сборке. Синхронные методы стали возможны, но злоупотреблять ими нельзя: синхронный вызов блокирует JS-поток на время работы натива.
Типы на границе
Codegen поддерживает примитивы (number, string, boolean), объекты с описанной формой, массивы, Promise, колбэки, enum. Большие бинарные данные гонять через границу всё равно дорого: файлы передают путями, а изображения обрабатывают на нативной стороне.
События из натива
Нативный код может отправлять события в JS: смена состояния SDK, прогресс загрузки. В спецификации TurboModule объявляют EventEmitter, на JS-стороне подписка возвращает объект с remove(). Подписку обязательно снимают в очистке useEffect, иначе обработчик держит компонент и вызывается после размонтирования.
Нативные компоненты
Свой нативный UI (карта, плеер, график на нативе) оформляют как Fabric-компонент: спецификация пропсов на TypeScript, Codegen, нативная реализация View. Пропсы типизированы, команды к компоненту (например, play()) описываются отдельно.
Expo Modules API
Альтернатива для тех, кто не хочет писать C++-склейку. Модуль описывается DSL на Swift и Kotlin:
class BatteryModule : Module() {
override fun definition() = ModuleDefinition {
Name("Battery")
AsyncFunction("getLevel") { readLevel() }
Events("onLowPower")
}
}
Expo Modules работают поверх JSI, поддерживают новую архитектуру и работают не только в Expo-проектах, но и в bare-приложениях после установки expo. Для многих команд это самый быстрый путь к своему модулю.
Потоки
Нативный метод не должен блокировать главный поток платформы долгой работой: обработку уводят в фоновый поток (корутины, DispatchQueue), а результат возвращают через промис. Обновление нативного UI, наоборот, делают на главном потоке.
Частые ошибки
- Писать модуль, когда есть готовая поддерживаемая библиотека.
- Синхронные методы для долгих операций.
- Передавать большие данные через границу вместо путей к файлам.
- Не снимать подписку на нативные события.
- Нет обработки ошибок: отклонять промис с понятным кодом.
Что спросят на собесе
- Как сделать нативный модуль в новой архитектуре: шаги.
- Что такое Codegen и что он проверяет.
- Чем TurboModule отличается от старого модуля.
- Как отправить событие из натива в JS.
- Expo Modules API: когда выбрать его.