$ cat posts/react-native-native-modules.md

# Integrações nativas em React Native - quando JavaScript não é suficiente

@date1 de dezembro de 2025
@read7 min de leitura

React Native abstrai muito bem a complexidade das plataformas nativas. Mas chega um momento em que você precisa ir além do que o JavaScript oferece. Seja para acessar uma API específica do sistema, integrar um SDK que só existe em Swift ou Kotlin, ou simplesmente extrair mais performance de uma operação crítica.

 

Neste artigo, vou explorar como funciona a comunicação entre JavaScript e código nativo, como criar seus próprios módulos e quando essa abordagem faz sentido.

 

A arquitetura por trás da ponte

Antes de escrever qualquer código nativo, é fundamental entender como React Native conecta os dois mundos. A arquitetura clássica usa uma ponte (bridge) assíncrona que serializa dados em JSON e os envia entre as threads JavaScript e nativa.

 

Na nova arquitetura (Fabric e TurboModules), essa comunicação evoluiu. TurboModules usam JSI (JavaScript Interface) para permitir chamadas síncronas e acesso direto a objetos C++, eliminando a serialização JSON e reduzindo drasticamente a latência.

 

Se você quer entender mais a fundo como funciona a arquitetura do React Native, recomendo a leitura do artigo Entendendo a Arquitetura do React Native.

 

Se você está criando um módulo novo, vale considerar TurboModules desde o início. O custo de implementação é maior, mas os ganhos de performance são significativos para operações frequentes.

 

Criando um módulo nativo no Android

Vamos criar um módulo simples que retorna o nível da bateria. Primeiro, criamos a classe do módulo em Kotlin:

 

// android/app/src/main/java/com/seuapp/BatteryModule.kt
package com.seuapp

import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise

class BatteryModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {

    override fun getName() = "BatteryModule"

    @ReactMethod
    fun getBatteryLevel(promise: Promise) {
        try {
            val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
            val batteryStatus = reactApplicationContext
                .registerReceiver(null, intentFilter)

            val level = batteryStatus?.getIntExtra(
                BatteryManager.EXTRA_LEVEL, -1
            ) ?: -1
            val scale = batteryStatus?.getIntExtra(
                BatteryManager.EXTRA_SCALE, -1
            ) ?: -1

            val batteryPct = level * 100 / scale.toFloat()
            promise.resolve(batteryPct.toDouble())
        } catch (e: Exception) {
            promise.reject("ERROR", e.message)
        }
    }
}

 

Depois, registramos o módulo em um Package:

 

// android/app/src/main/java/com/seuapp/BatteryPackage.kt
package com.seuapp

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class BatteryPackage : ReactPackage {
    override fun createNativeModules(
        reactContext: ReactApplicationContext
    ): List<NativeModule> {
        return listOf(BatteryModule(reactContext))
    }

    override fun createViewManagers(
        reactContext: ReactApplicationContext
    ): List<ViewManager<*, *>> = emptyList()
}

 

Por fim, adicionamos o package na lista de packages do MainApplication:

 

// android/app/src/main/java/com/seuapp/MainApplication.kt
package com.seuapp

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost

class MainApplication : Application(), ReactApplication {

    override val reactNativeHost: ReactNativeHost =
        object : DefaultReactNativeHost(this) {
            override fun getPackages(): List<ReactPackage> =
                PackageList(this).packages.apply {
                    add(BatteryPackage()) // Adicione seu package aqui
                }

            override fun getJSMainModuleName(): String = "index"
            override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
            override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
            override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
        }

    override val reactHost: ReactHost
        get() = getDefaultReactHost(applicationContext, reactNativeHost)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
            load()
        }
    }
}

 

O ponto chave é adicionar BatteryPackage() dentro do método getPackages(). É ali que o React Native descobre quais módulos nativos estão disponíveis.

 

Criando o mesmo módulo no iOS

No iOS, criamos o módulo em Swift. Primeiro, o arquivo de implementação:

 

// ios/BatteryModule.swift
import Foundation
import UIKit

@objc(BatteryModule)
class BatteryModule: NSObject {

    @objc
    func getBatteryLevel(
        _ resolve: @escaping RCTPromiseResolveBlock,
        rejecter reject: @escaping RCTPromiseRejectBlock
    ) {
        DispatchQueue.main.async {
            UIDevice.current.isBatteryMonitoringEnabled = true
            let batteryLevel = UIDevice.current.batteryLevel

            if batteryLevel < 0 {
                reject("ERROR", "Não foi possível obter nível da bateria", nil)
            } else {
                resolve(Double(batteryLevel * 100))
            }
        }
    }

    @objc
    static func requiresMainQueueSetup() -> Bool {
        return false
    }
}

 

E o arquivo de macro para expor ao React Native:

 

// ios/BatteryModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(BatteryModule, NSObject)

RCT_EXTERN_METHOD(getBatteryLevel:
    (RCTPromiseResolveBlock)resolve
    rejecter:(RCTPromiseRejectBlock)reject
)

@end

 

Consumindo o módulo no JavaScript

Com o módulo registrado em ambas as plataformas, o uso no JavaScript é direto:

 

import { NativeModules } from 'react-native';

const { BatteryModule } = NativeModules;

async function checkBattery() {
    try {
        const level = await BatteryModule.getBatteryLevel();
        console.log(`Bateria: ${level}%`);
    } catch (error) {
        console.error('Erro ao obter bateria:', error);
    }
}

 

Para projetos maiores, vale criar um wrapper TypeScript com tipos bem definidos e tratamento de erros centralizado.

 

Enviando eventos do nativo para JavaScript

Muitas vezes, o fluxo é inverso: o código nativo precisa notificar o JavaScript sobre algo que aconteceu. Para isso, usamos event emitters.

 

No Android:

 

private fun sendEvent(eventName: String, params: WritableMap?) {
    reactApplicationContext
        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
        .emit(eventName, params)
}

 

No iOS:

 

@objc(BatteryModule)
class BatteryModule: RCTEventEmitter {

    override func supportedEvents() -> [String]! {
        return ["onBatteryChange"]
    }

    func notifyBatteryChange(level: Float) {
        sendEvent(withName: "onBatteryChange", body: ["level": level])
    }
}

 

No JavaScript, escutamos o evento:

 

import { NativeEventEmitter, NativeModules } from 'react-native';

const { BatteryModule } = NativeModules;
const eventEmitter = new NativeEventEmitter(BatteryModule);

useEffect(() => {
    const subscription = eventEmitter.addListener(
        'onBatteryChange',
        (event) => {
            console.log('Bateria mudou:', event.level);
        }
    );

    return () => subscription.remove();
}, []);

 

Quando criar um módulo nativo

Antes de partir para código nativo, avalie se realmente precisa. Existem cenários claros onde faz sentido:

 

  • Performance crítica: operações que precisam rodar em menos de 16ms para não travar a UI.
  • APIs de sistema: acesso a recursos como Bluetooth LE, NFC, sensores específicos, ou APIs de acessibilidade.
  • SDKs proprietários: integração com SDKs de pagamento, analytics ou autenticação que só existem em código nativo.
  • Processamento pesado: criptografia, compressão de imagem, ou manipulação de vídeo.

 

Por outro lado, evite criar módulos nativos para coisas que bibliotecas existentes já resolvem bem. A manutenção de código nativo em duas plataformas tem um custo significativo.

 

TurboModules e a nova arquitetura

Se você está em um projeto que já migrou para a nova arquitetura, TurboModules oferecem vantagens importantes:

 

  • Lazy loading: módulos só são carregados quando usados pela primeira vez.
  • Chamadas síncronas: quando necessário, você pode fazer chamadas síncronas ao código nativo.
  • Tipagem estática: a especificação do módulo é feita em TypeScript, gerando código nativo automaticamente.

 

A implementação é mais verbosa, mas o tooling do React Native gera boa parte do boilerplate. Vale o investimento para módulos que serão usados frequentemente.

 

Debugging e armadilhas comuns

Trabalhar com código nativo traz desafios próprios de debugging:

 

  • Logs nativos: use Logcat (Android) e Console (Xcode) para ver logs do código nativo. Eles não aparecem no Metro.
  • Thread safety: chamadas do JavaScript chegam em uma thread específica. Operações de UI devem ser despachadas para a main thread.
  • Ciclo de vida: módulos nativos podem ser recriados durante reloads. Limpe recursos no método invalidate.
  • Tipos de dados: a ponte converte tipos automaticamente, mas nem tudo é suportado. Maps e Arrays funcionam, mas objetos complexos precisam ser serializados.

 

Conclusão

Integrações nativas são uma ferramenta poderosa no arsenal de um desenvolvedor React Native. Elas permitem ir além das limitações do JavaScript e acessar todo o potencial das plataformas.

 

Mas com esse poder vem responsabilidade. Código nativo significa manter duas implementações, lidar com peculiaridades de cada plataforma, e debugging mais complexo. Use quando necessário, mas não por padrão.

 

Se você trabalha em apps que exigem performance ou integrações específicas, dominar módulos nativos vai te diferenciar. É uma habilidade que separa desenvolvedores React Native iniciantes dos avançados.