Módulos Nativos en React Native 2026: Expo Modules API, Turbo Modules y Código Nativo Paso a Paso

Crea tus propios módulos nativos en React Native con Expo Modules API y Turbo Modules. Guía práctica con ejemplos completos en Swift y Kotlin, desde lo básico hasta eventos y vistas nativas.

Por qué necesitas entender los módulos nativos (aunque uses Expo)

React Native te permite construir apps móviles con JavaScript y React. Genial. Pero llega un momento — y te lo digo por experiencia — en el que JavaScript simplemente no alcanza. Necesitas acceder al sensor de huellas con una API personalizada, integrar un SDK de pagos que solo existe en Swift/Kotlin, procesar imágenes a nivel nativo para rendimiento bruto, o comunicarte con hardware Bluetooth de una forma que ninguna librería existente cubre.

Ese momento solía ser aterrador. El viejo sistema de bridge era lento, verboso y requería escribir Objective-C (sí, Objective-C, no Swift) para iOS. En Android, Java era la norma aunque Kotlin ya dominaba el ecosistema. La experiencia de desarrollo era, honestamente, bastante dolorosa.

Pero en 2026, las cosas han cambiado mucho. Ahora tienes dos caminos modernos para crear módulos nativos:

  • Expo Modules API: La forma más sencilla y la que yo recomendaría para la mayoría de proyectos. Escribes Swift y Kotlin idiomáticos con una API declarativa elegante. Funciona dentro del ecosistema Expo y se integra con EAS Build sin fricción.
  • Turbo Modules: Parte de la Nueva Arquitectura de React Native. Comunicación directa con JavaScript sin serialización JSON — puro rendimiento a través de JSI (JavaScript Interface). Para cuando cada milisegundo importa.

En esta guía vamos a recorrer ambos caminos con ejemplos prácticos completos. Al final, vas a poder crear tu propio módulo nativo desde cero y vas a entender cuándo conviene usar cada enfoque.

Panorama de módulos nativos en 2026: qué ha cambiado

Para poner las cosas en perspectiva, veamos cómo ha evolucionado la comunicación entre JavaScript y código nativo en React Native.

El viejo Bridge (Legacy)

El sistema original usaba un puente asíncrono que serializaba todos los datos a JSON. Cada llamada entre JavaScript y código nativo pasaba por este cuello de botella. Funcionaba, pero con latencia medible — especialmente problemática para operaciones frecuentes como animaciones o procesamiento en tiempo real.

Además, la API requería clases Objective-C con macros como RCT_EXPORT_METHOD que eran un dolor de cabeza para debuggear.

Turbo Modules + JSI (Nueva Arquitectura)

Con la Nueva Arquitectura (habilitada por defecto desde React Native 0.76), los Turbo Modules se comunican directamente con JavaScript a través de JSI — sin serialización JSON, sin puente asíncrono. Las funciones nativas se pueden invocar síncronamente, y los tipos se validan con Codegen a partir de specs en TypeScript/Flow. El resultado es latencia casi nula y type-safety real en la frontera JS-nativo.

Expo Modules API

Expo tomó un enfoque diferente: en vez de modificar el sistema de comunicación, simplificaron radicalmente la experiencia del desarrollador. Con Expo Modules API escribes Swift puro y Kotlin puro, usando una API declarativa inspirada en SwiftUI. No necesitas Codegen, no necesitas specs, y funciona tanto en proyectos Expo como en bare React Native. Bastante elegante, la verdad.

¿Cuál elegir?

Criterio Expo Modules API Turbo Modules
Facilidad de desarrollo Alta — API declarativa, menos boilerplate Media — requiere specs de Codegen
Lenguajes nativos Swift + Kotlin idiomáticos Objective-C++/Swift + Java/Kotlin
Rendimiento Excelente — usa JSI internamente Máximo — control total de JSI
Integración Expo Nativa — funciona con EAS Build Compatible pero sin soporte especial
Llamadas síncronas
Soporte de eventos Sí — sistema de eventos integrado Sí — con EventEmitter
Uso recomendado 95% de los casos SDKs de alto rendimiento, librerías de infraestructura

Mi recomendación: si estás desarrollando una app y necesitas un módulo nativo, ve con Expo Modules API. Si estás creando una librería de infraestructura para la comunidad o necesitas control absoluto de la comunicación JS-nativo, ahí es donde Turbo Modules tiene sentido.

Expo Modules API: tu primer módulo nativo paso a paso

Vamos a crear algo real. Un módulo que obtiene información del dispositivo a nivel nativo: modelo exacto, memoria RAM disponible y nivel de batería. Son datos que las APIs de JavaScript no pueden obtener con precisión.

Paso 1: Crear la estructura del módulo

Expo te da un comando para generar toda la estructura del módulo de una vez:

# Desde la raíz de tu proyecto Expo
npx create-expo-module@latest expo-device-info-native --local

El flag --local crea el módulo dentro de tu proyecto (en ./modules/expo-device-info-native/) en lugar de generar un paquete npm independiente. Perfecto para módulos específicos de tu app.

La estructura que se genera es esta:

modules/
└── expo-device-info-native/
    ├── index.ts                    # API JavaScript pública
    ├── src/
    │   └── ExpoDeviceInfoNativeModule.ts  # Definición del módulo
    ├── ios/
    │   └── ExpoDeviceInfoNativeModule.swift  # Implementación iOS
    ├── android/
    │   └── src/main/java/expo/modules/deviceinfonative/
    │       └── ExpoDeviceInfoNativeModule.kt  # Implementación Android
    └── expo-module.config.json     # Configuración del módulo

Paso 2: Definir la API JavaScript

Primero, definimos lo que nuestro módulo va a exponer a JavaScript. Edita modules/expo-device-info-native/index.ts:

import ExpoDeviceInfoNativeModule from "./src/ExpoDeviceInfoNativeModule";

export type DeviceInfo = {
  model: string;
  osVersion: string;
  totalMemoryMB: number;
  availableMemoryMB: number;
  batteryLevel: number;
  isCharging: boolean;
};

export function getDeviceInfo(): DeviceInfo {
  return ExpoDeviceInfoNativeModule.getDeviceInfo();
}

export function getBatteryLevel(): number {
  return ExpoDeviceInfoNativeModule.getBatteryLevel();
}

Y el archivo de definición del módulo en src/ExpoDeviceInfoNativeModule.ts:

import { requireNativeModule } from "expo-modules-core";

export default requireNativeModule("ExpoDeviceInfoNative");

Paso 3: Implementación en Swift (iOS)

Aquí es donde Expo Modules API realmente brilla. Mira lo limpio que queda el código:

// ios/ExpoDeviceInfoNativeModule.swift
import ExpoModulesCore
import UIKit

public class ExpoDeviceInfoNativeModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ExpoDeviceInfoNative")

    Function("getDeviceInfo") { () -> [String: Any] in
      UIDevice.current.isBatteryMonitoringEnabled = true
      let processInfo = ProcessInfo.processInfo

      return [
        "model": self.getDeviceModel(),
        "osVersion": UIDevice.current.systemVersion,
        "totalMemoryMB": Int(processInfo.physicalMemory / 1_048_576),
        "availableMemoryMB": self.getAvailableMemory(),
        "batteryLevel": Double(UIDevice.current.batteryLevel * 100),
        "isCharging": UIDevice.current.batteryState == .charging
          || UIDevice.current.batteryState == .full
      ]
    }

    Function("getBatteryLevel") { () -> Double in
      UIDevice.current.isBatteryMonitoringEnabled = true
      return Double(UIDevice.current.batteryLevel * 100)
    }
  }

  private func getDeviceModel() -> String {
    var systemInfo = utsname()
    uname(&systemInfo)
    let machineMirror = Mirror(reflecting: systemInfo.machine)
    let identifier = machineMirror.children.reduce("") { identifier, element in
      guard let value = element.value as? Int8, value != 0 else { return identifier }
      return identifier + String(UnicodeScalar(UInt8(value)))
    }
    return identifier
  }

  private func getAvailableMemory() -> Int {
    var pageSize: vm_size_t = 0
    let hostPort: mach_port_t = mach_host_self()
    host_page_size(hostPort, &pageSize)

    var vmStats = vm_statistics64()
    var count = mach_msg_type_number_t(
      MemoryLayout.size / MemoryLayout.size
    )

    let result = withUnsafeMutablePointer(to: &vmStats) {
      $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
        host_statistics64(hostPort, HOST_VM_INFO64, $0, &count)
      }
    }

    guard result == KERN_SUCCESS else { return 0 }
    let freeMemory = UInt64(vmStats.free_count) * UInt64(pageSize)
    return Int(freeMemory / 1_048_576)
  }
}

Fíjate en lo que NO tuviste que hacer: nada de RCT_EXPORT_MODULE(), nada de Objective-C, nada de macros crípticas. Es Swift puro con una API declarativa. Name() define el nombre del módulo, Function() expone funciones a JavaScript. Así de simple.

Paso 4: Implementación en Kotlin (Android)

// android/src/main/java/expo/modules/deviceinfonative/ExpoDeviceInfoNativeModule.kt
package expo.modules.deviceinfonative

import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class ExpoDeviceInfoNativeModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("ExpoDeviceInfoNative")

    Function("getDeviceInfo") {
      val context = appContext.reactContext ?: throw Error("Context no disponible")
      val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
      val memoryInfo = ActivityManager.MemoryInfo()
      activityManager.getMemoryInfo(memoryInfo)

      val batteryStatus = getBatteryStatus(context)

      mapOf(
        "model" to "${Build.MANUFACTURER} ${Build.MODEL}",
        "osVersion" to Build.VERSION.RELEASE,
        "totalMemoryMB" to (memoryInfo.totalMem / 1_048_576),
        "availableMemoryMB" to (memoryInfo.availMem / 1_048_576),
        "batteryLevel" to batteryStatus.first,
        "isCharging" to batteryStatus.second
      )
    }

    Function("getBatteryLevel") {
      val context = appContext.reactContext ?: throw Error("Context no disponible")
      getBatteryStatus(context).first
    }
  }

  private fun getBatteryStatus(context: Context): Pair {
    val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
    val batteryStatus = context.registerReceiver(null, intentFilter)

    val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
    val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
    val batteryPct = if (level >= 0 && scale > 0) (level.toDouble() / scale * 100) else 0.0

    val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
    val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING
      || status == BatteryManager.BATTERY_STATUS_FULL

    return Pair(batteryPct, isCharging)
  }
}

Misma filosofía: Kotlin idiomático con la misma API declarativa ModuleDefinition. Este paralelismo entre Swift y Kotlin es intencional — Expo diseñó la API para que ambas plataformas se sientan casi idénticas, lo cual hace que mantener el código sea mucho más llevadero.

Paso 5: Usar el módulo en tu app

// app/device-info.tsx
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useState } from "react";
import { getDeviceInfo, type DeviceInfo } from "@/modules/expo-device-info-native";

export default function DeviceInfoScreen() {
  const [info, setInfo] = useState<DeviceInfo | null>(null);

  const fetchInfo = () => {
    const deviceInfo = getDeviceInfo();
    setInfo(deviceInfo);
  };

  return (
    <View style={styles.container}>
      <Pressable style={styles.button} onPress={fetchInfo}>
        <Text style={styles.buttonText}>Obtener info del dispositivo</Text>
      </Pressable>

      {info && (
        <View style={styles.card}>
          <Text style={styles.label}>Modelo: <Text style={styles.value}>{info.model}</Text></Text>
          <Text style={styles.label}>OS: <Text style={styles.value}>{info.osVersion}</Text></Text>
          <Text style={styles.label}>RAM Total: <Text style={styles.value}>{info.totalMemoryMB} MB</Text></Text>
          <Text style={styles.label}>RAM Disponible: <Text style={styles.value}>{info.availableMemoryMB} MB</Text></Text>
          <Text style={styles.label}>Batería: <Text style={styles.value}>{info.batteryLevel.toFixed(1)}%</Text></Text>
          <Text style={styles.label}>Cargando: <Text style={styles.value}>{info.isCharging ? "Sí" : "No"}</Text></Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20, justifyContent: "center" },
  button: {
    backgroundColor: "#6C63FF",
    padding: 16,
    borderRadius: 12,
    alignItems: "center",
    marginBottom: 20,
  },
  buttonText: { color: "#fff", fontSize: 16, fontWeight: "700" },
  card: {
    backgroundColor: "#F5F5F5",
    padding: 20,
    borderRadius: 12,
    gap: 8,
  },
  label: { fontSize: 15, color: "#666" },
  value: { fontWeight: "700", color: "#333" },
});

Un detalle importante: la llamada getDeviceInfo() es síncrona. No es una promesa. El valor se devuelve al instante porque Expo Modules API usa JSI internamente. Esto es un cambio enorme respecto al viejo bridge asíncrono.

Paso 6: Build y prueba

Los módulos nativos no funcionan en Expo Go — vas a necesitar un development build:

# Crear un development build local
npx expo run:ios
# o
npx expo run:android

# O usar EAS Build para builds en la nube
eas build --profile development --platform ios

Funcionalidades avanzadas de Expo Modules API

El ejemplo anterior cubre lo básico, pero Expo Modules API tiene bastante más que ofrecer. Veamos las funcionalidades que probablemente necesitarás en proyectos de verdad.

Funciones asíncronas

Para operaciones que toman tiempo (red, filesystem, procesamiento pesado), usa AsyncFunction:

// Swift
AsyncFunction("processImage") { (uri: String) -> [String: Any] in
  let image = try await loadImage(from: uri)
  let processed = try await applyFilter(to: image)
  let savedUri = try await saveImage(processed)
  return [
    "uri": savedUri,
    "width": processed.size.width,
    "height": processed.size.height
  ]
}
// Kotlin
AsyncFunction("processImage") { uri: String ->
  val image = loadImage(uri)
  val processed = applyFilter(image)
  val savedUri = saveImage(processed)
  mapOf(
    "uri" to savedUri,
    "width" to processed.width,
    "height" to processed.height
  )
}

Eventos: comunicación nativo → JavaScript

A veces el código nativo necesita notificar a JavaScript de algo que pasó — un cambio de estado, un dato recibido, una descarga que terminó. Para eso Expo Modules API incluye un sistema de eventos bastante cómodo:

// Swift — emisor de eventos
public class LocationModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ExpoLocation")

    Events("onLocationUpdate", "onLocationError")

    Function("startTracking") {
      self.locationManager.startUpdating()
    }

    Function("stopTracking") {
      self.locationManager.stopUpdating()
    }
  }

  // Llamado desde el delegate de CoreLocation
  func didUpdateLocation(_ location: CLLocation) {
    sendEvent("onLocationUpdate", [
      "latitude": location.coordinate.latitude,
      "longitude": location.coordinate.longitude,
      "accuracy": location.horizontalAccuracy
    ])
  }
}
// JavaScript — receptor de eventos
import { useEvent } from "expo";
import ExpoLocation from "./ExpoLocationModule";

function LocationTracker() {
  const location = useEvent(ExpoLocation, "onLocationUpdate");

  useEffect(() => {
    ExpoLocation.startTracking();
    return () => ExpoLocation.stopTracking();
  }, []);

  if (!location) return <Text>Esperando ubicación...</Text>;

  return (
    <Text>
      Lat: {location.latitude}, Lon: {location.longitude}
    </Text>
  );
}

El hook useEvent de Expo se encarga automáticamente de la suscripción y desuscripción. No necesitas gestionar listeners manualmente — nada de addListener y removeListener por tu cuenta.

Vistas nativas (Native Views)

Además de funciones, puedes crear componentes de UI nativos que se renderizan directamente en la jerarquía de vistas de cada plataforma:

// Swift
View(ExpoNativeMapView.self) {
  Prop("region") { (view, region: [String: Double]) in
    view.setRegion(
      latitude: region["latitude"] ?? 0,
      longitude: region["longitude"] ?? 0,
      latDelta: region["latDelta"] ?? 0.01,
      lonDelta: region["lonDelta"] ?? 0.01
    )
  }

  Events("onMarkerPress")
}
// JavaScript
import { requireNativeViewManager } from "expo-modules-core";

const NativeMapView = requireNativeViewManager("ExpoNativeMap");

export function MapView({ region, onMarkerPress }) {
  return (
    <NativeMapView
      region={region}
      onMarkerPress={onMarkerPress}
      style={{ flex: 1 }}
    />
  );
}

Turbo Modules: rendimiento máximo con la Nueva Arquitectura

Si Expo Modules API cubre el 95% de los casos, ¿cuándo realmente necesitas Turbo Modules? Básicamente, cuando estás creando una librería que requiere:

  • Control directo sobre JSI y la capa C++
  • Lazy loading nativo — el módulo solo se carga cuando se usa por primera vez
  • Type-safety estricto generado automáticamente con Codegen
  • Rendimiento absoluto para operaciones de alta frecuencia

Crear una spec de Turbo Module

Todo Turbo Module empieza con una spec en TypeScript que define el contrato entre JavaScript y código nativo. Piénsalo como una interfaz que ambos lados deben respetar:

// specs/NativeCryptoModule.ts
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";

export interface Spec extends TurboModule {
  hashSHA256(input: string): string;  // síncrono
  encryptAES(plaintext: string, key: string): Promise<string>;  // asíncrono
  generateKeyPair(): Promise<{ publicKey: string; privateKey: string }>;
}

export default TurboModuleRegistry.getEnforcing<Spec>("NativeCrypto");

Esta spec es el contrato. Codegen la procesa y genera interfaces en C++, Objective-C++ y Java que tu implementación debe cumplir. Si tu implementación no coincide, obtienes errores en tiempo de compilación — no en runtime. Y eso, créeme, te ahorra horas de debugging.

Implementación Android (Kotlin)

// android/src/main/java/com/nativecrypto/NativeCryptoModule.kt
package com.nativecrypto

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableNativeMap
import com.nativecrypto.NativeCryptoSpec
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec

class NativeCryptoModule(reactContext: ReactApplicationContext) :
  NativeCryptoSpec(reactContext) {

  override fun getName() = "NativeCrypto"

  override fun hashSHA256(input: String): String {
    val digest = MessageDigest.getInstance("SHA-256")
    val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
    return hash.joinToString("") { "%02x".format(it) }
  }

  override fun encryptAES(plaintext: String, key: String, promise: Promise) {
    try {
      val secretKey = SecretKeySpec(key.toByteArray().copyOf(32), "AES")
      val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
      cipher.init(Cipher.ENCRYPT_MODE, secretKey)
      val encrypted = cipher.doFinal(plaintext.toByteArray())
      promise.resolve(Base64.getEncoder().encodeToString(encrypted))
    } catch (e: Exception) {
      promise.reject("ENCRYPTION_ERROR", e.message, e)
    }
  }

  override fun generateKeyPair(promise: Promise) {
    try {
      val keyGen = KeyPairGenerator.getInstance("RSA")
      keyGen.initialize(2048)
      val pair = keyGen.generateKeyPair()
      val result = WritableNativeMap().apply {
        putString("publicKey", Base64.getEncoder().encodeToString(pair.public.encoded))
        putString("privateKey", Base64.getEncoder().encodeToString(pair.private.encoded))
      }
      promise.resolve(result)
    } catch (e: Exception) {
      promise.reject("KEYGEN_ERROR", e.message, e)
    }
  }
}

Diferencias clave vs Expo Modules API

Hay diferencias fundamentales que vale la pena tener claras:

  • Turbo Modules requieren una spec de TypeScript procesada por Codegen
  • Heredas de una clase generada (NativeCryptoSpec) en vez de usar ModuleDefinition
  • Las funciones asíncronas reciben un Promise como parámetro explícito
  • Necesitas registrar el módulo manualmente en un ReactPackage
  • El setup es más verboso, sí, pero a cambio tienes control total

Módulos locales vs paquetes npm: cuándo usar cada formato

Cuando creas un módulo nativo, tienes dos opciones de estructura. La elección depende bastante de si el módulo es solo para tu app o si planeas compartirlo.

Módulos locales (dentro del proyecto)

# Crear módulo local
npx create-expo-module@latest mi-modulo --local

El módulo vive en ./modules/mi-modulo/ dentro de tu proyecto. Ideal para:

  • Funcionalidad específica de tu app que no necesita compartirse
  • Prototipos rápidos cuando estás explorando una idea
  • Integración de SDKs propietarios de un cliente

Paquetes npm (librerías independientes)

# Crear paquete independiente
npx create-expo-module@latest mi-libreria

El módulo se crea como un proyecto independiente que puedes publicar en npm. Funciona bien para:

  • Librerías que compartirás con la comunidad
  • Módulos que se usan en múltiples apps de tu empresa
  • Paquetes open source

Migración del viejo Bridge a APIs modernas

Si tu proyecto todavía tiene módulos nativos escritos con el viejo bridge, migrar es altamente recomendable. La diferencia en experiencia de desarrollo (y en rendimiento) es notable. Aquí va una guía rápida.

Antes (Bridge Legacy — Objective-C)

// MyOldModule.m
#import <React/RCTBridgeModule.h>

@interface MyOldModule : NSObject <RCTBridgeModule>
@end

@implementation MyOldModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(getValue:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  resolve(@{@"value": @42});
}

@end

Después (Expo Modules API — Swift)

// MyNewModule.swift
import ExpoModulesCore

public class MyNewModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyModule")

    Function("getValue") { () -> [String: Int] in
      return ["value": 42]
    }
  }
}

El código resultante es más corto, más legible, y es Swift moderno. Al migrar también ganas acceso a todas las funcionalidades avanzadas — eventos, vistas nativas, constantes — sin boilerplate adicional.

Pasos de migración

  1. Crea la estructura del nuevo módulo con create-expo-module
  2. Traslada la lógica nativa a Swift/Kotlin usando la API declarativa
  3. Actualiza las importaciones en JavaScript para apuntar al nuevo módulo
  4. Prueba en ambas plataformas — no te saltes este paso, en serio
  5. Elimina el código legacy una vez que todo funcione correctamente

Depuración de módulos nativos: herramientas y técnicas

Debuggear módulos nativos requiere herramientas diferentes a las de JavaScript. Esto es algo que puede ser frustrante al principio, pero una vez que te acostumbras al workflow se vuelve bastante manejable.

Logs nativos

# iOS — ver logs de Xcode desde terminal
xcrun simctl spawn booted log stream --predicate "subsystem == 'com.yourapp'" --level debug

# Android — Logcat filtrado
adb logcat -s ExpoDeviceInfoNative:V ReactNative:V

Xcode y Android Studio

Para depuración seria de código nativo, no hay sustituto para los IDEs nativos:

  • Xcode: Abre el workspace de iOS (ios/YourApp.xcworkspace), pon breakpoints en tu código Swift y ejecuta desde ahí.
  • Android Studio: Abre la carpeta android/ como proyecto, pon breakpoints en Kotlin y usa el debugger.

Errores comunes y soluciones

Error Causa Solución
Cannot find native module El módulo no está registrado o no se compiló Ejecuta npx expo run:ios para recompilar
Module not found in Expo Go Los módulos nativos no funcionan en Expo Go Usa un development build con npx expo run:*
Swift compiler error Tipos incompatibles o API incorrecta Verifica los tipos de retorno y firmas de función
Codegen type mismatch La spec no coincide con la implementación Regenera con npx react-native codegen

Mejores prácticas para módulos nativos en 2026

Después de trabajar en varios proyectos con módulos nativos, estas son las prácticas que más impacto tienen en la calidad del resultado:

  • Mantén la superficie de API mínima. Expón solo lo que JavaScript realmente necesita. Cuanto más grande sea la API, más difícil de mantener y testear.
  • Haz operaciones pesadas asíncronas. Si una operación nativa puede tardar más de unos milisegundos, usa AsyncFunction. Bloquear el hilo de JS con una función síncrona pesada causa jank visible y tus usuarios lo van a notar.
  • Valida entradas del lado nativo. No confíes ciegamente en los datos que llegan de JavaScript. Valida tipos, rangos y formatos antes de usarlos.
  • Documenta los tipos con TypeScript. Tu archivo index.ts es el contrato público del módulo. Tipos claros y bien exportados funcionan como documentación viva.
  • Testea en ambas plataformas siempre. Un módulo que va perfecto en iOS puede fallar en Android y viceversa. No asumas paridad — te lo digo por las malas.
  • Prefiere Expo Modules API por defecto. Solo recurre a Turbo Modules cuando necesites rendimiento extremo o control de bajo nivel que Expo Modules API no te da.

Preguntas frecuentes

¿Los módulos nativos funcionan con Expo Go?

No. Expo Go incluye un conjunto fijo de módulos nativos predefinidos. Si creas tu propio módulo, necesitas un development build personalizado. Lo creas localmente con npx expo run:ios o npx expo run:android, o en la nube con EAS Build usando eas build --profile development. El development build funciona igual que Expo Go pero incluye tus módulos nativos.

¿Puedo usar Expo Modules API sin Expo como framework?

Sí, totalmente. Expo Modules API funciona en proyectos bare React Native que no usan Expo Router ni ninguna otra parte del framework. Solo necesitas instalar expo-modules-core y configurar los archivos nativos. La documentación oficial de Expo tiene instrucciones específicas para esta integración.

¿Cuánto más rápidos son los Turbo Modules comparados con el viejo bridge?

Mucho. El viejo bridge serializaba todo a JSON y lo enviaba de forma asíncrona, con latencia de entre 1-5ms por llamada. Los Turbo Modules, gracias a JSI, permiten llamadas síncronas con latencia de microsegundos — estamos hablando de 10-100x más rápidos para operaciones individuales. Para operaciones frecuentes como actualizaciones de animaciones o streams de datos, esta diferencia es la que separa una app fluida de una con jank perceptible.

¿Qué lenguajes necesito saber para crear módulos nativos?

Con Expo Modules API necesitas Swift para iOS y Kotlin para Android. No necesitas Objective-C ni Java — la API soporta los lenguajes modernos directamente. Eso sí, un conocimiento básico de las APIs nativas de cada plataforma (UIKit en iOS, Android SDK en Android) es necesario para implementar lo que quieras exponer.

¿Cómo publico un módulo nativo como paquete npm?

Crea el módulo con npx create-expo-module@latest mi-libreria (sin el flag --local). Esto genera un proyecto independiente con su propio package.json. Desarrolla y prueba en el proyecto de ejemplo que se incluye, configura los campos de package.json correctamente (main, types, repository), y publica con npm publish. Los usuarios lo instalarán con npx expo install tu-paquete y funcionará automáticamente con EAS Build.

Sobre el Autor Editorial Team

Our team of expert writers and editors.