Expo Modules API: Come Creare Moduli Nativi React Native nel 2026

Guida pratica all'Expo Modules API nel 2026: scrivi moduli nativi React Native in Swift e Kotlin con un DSL dichiarativo che genera TurboModule e Fabric Component automaticamente, senza toccare Objective-C++ o file di Codegen.

Expo Modules API: Moduli Nativi 2026

Aggiornato: 29 giugno 2026

L'Expo Modules API è il modo ufficiale e raccomandato per scrivere moduli nativi React Native nel 2026: una API dichiarativa Swift/Kotlin che genera automaticamente il binding TurboModule sopra la Nuova Architettura, senza scrivere Objective-C++, header JSI o file di Codegen a mano. In pratica, dichiari funzioni, proprietà ed eventi in un DSL conciso e ottieni una libreria type-safe usabile in JavaScript con requireNativeModule(). Vengo dal web e onestamente è il primo strumento che fa sembrare il codice nativo familiare come un hook React.

  • L'Expo Modules API genera automaticamente i wrapper TurboModule per la Nuova Architettura, eliminando boilerplate Objective-C++ e JNI.
  • Si usano Function, AsyncFunction, Property, Events e View in un DSL Swift/Kotlin per definire l'interfaccia pubblica del modulo.
  • Il comando npx create-expo-module crea uno scheletro multipiattaforma con TypeScript, esempio funzionante e configurazione iOS/Android pronta.
  • I moduli funzionano sia in app Expo gestite (tramite config plugin) sia in progetti bare React Native dopo l'installazione di expo-modules-core.
  • L'API supporta view nativi (SwiftUI/Jetpack Compose), eventi continui, conversione automatica dei tipi e iniezione del Permissions Manager.
  • Compatibile con New Architecture (Fabric/TurboModules) abilitata di default da React Native 0.78 in poi.

Cos'è l'Expo Modules API e quando usarla

L'Expo Modules API è un framework dichiarativo per scrivere codice nativo iOS (Swift) e Android (Kotlin) che viene esposto a JavaScript come un modulo TurboModule completamente tipizzato. Il framework si occupa del marshalling dei tipi, del threading, della propagazione delle eccezioni e dell'integrazione con la Nuova Architettura React Native. Da sviluppatore web, lo vedo come l'equivalente mobile di una API server che restituisce dati strutturati: scrivi una funzione, dichiari i parametri, e il chiamante JS riceve una Promise tipizzata.

Usa l'Expo Modules API ogni volta che devi accedere a hardware o SDK piattaforma non coperti dai pacchetti Expo esistenti: lettori biometrici personalizzati, integrazione con SDK di terze parti (Stripe Terminal, ARKit, Google Cast), wrapper di librerie iOS/Android proprietarie, o widget di sistema come Live Activities su iOS e Glance Widgets su Android. Se ti basta una richiesta HTTP o storage chiave-valore esistono già pacchetti dedicati come MMKV ed Expo SQLite per lo storage offline e non serve scrivere nulla a basso livello.

La compatibilità con Bare React Native è totale: l'unico requisito è che il progetto abbia expo-modules-core installato, cosa che npx install-expo-modules fa automaticamente. Non serve usare Expo Go né EAS per beneficiarne, e funziona perfettamente con progetti generati da npx react-native init degli anni precedenti che hanno migrato alla New Architecture.

Expo Modules API vs TurboModules manuali vs Bridge API legacy

Capire le differenze tra i tre approcci aiuta a scegliere lo strumento giusto. La vecchia Bridge API (basata su JSON serializzato e ponte asincrono) è deprecata dalla Nuova Architettura; i TurboModules scritti a mano richiedono header C++/Objective-C++, file codegenSchema e implementazione separata per iOS e Android. L'Expo Modules API genera questo livello automaticamente.

CaratteristicaExpo Modules APITurboModules manualiBridge API legacy
LinguaggiSwift, KotlinC++, Obj-C++, JavaObj-C, Java
Boilerplate per metodo1 riga~30 righe (spec + impl)~10 righe
Tipizzazione TypeScriptAutomatica via codegen internoManuale via Codegen RNManuale, spesso assente
SincronicitàSync e asyncSync e asyncSolo async
Nuova ArchitetturaNativaNativaSolo via interop layer
View nativiSwiftUI/Jetpack Compose supportatiSolo Fabric ComponentsViewManagers legacy
Curva di apprendimentoBassa (DSL leggibile)Alta (Codegen, JSI)Media

Per chi viene dalla scrittura di hook React, il DSL di Expo Modules è quello che assomiglia di più a una funzione dichiarativa: niente lifecycle objects, niente @ReactMethod, niente RCT_EXPORT_MODULE. Per uno sguardo più profondo all'infrastruttura sottostante (JSI, Fabric, Codegen) consiglio la nostra guida alla Nuova Architettura React Native.

Come creare un modulo nativo con create-expo-module

Il modo più rapido per partire è il template ufficiale. Apri un terminale fuori dal tuo progetto principale ed esegui:

npx create-expo-module@latest expo-pulse-sensor

cd expo-pulse-sensor
npm install
cd example
npx expo run:ios

Il template genera una struttura multipiattaforma identica per ogni nuovo modulo:

expo-pulse-sensor/
├── android/
│   └── src/main/java/expo/modules/pulsesensor/
│       └── ExpoPulseSensorModule.kt
├── ios/
│   └── ExpoPulseSensorModule.swift
├── src/
│   ├── ExpoPulseSensor.types.ts
│   ├── ExpoPulseSensorModule.ts
│   ├── ExpoPulseSensorView.tsx
│   └── index.ts
├── example/              # App di esempio in Expo
├── expo-module.config.json
└── package.json

Il file expo-module.config.json dichiara le piattaforme supportate e il nome del modulo. Da qui in poi modifichi solo i sorgenti Swift, Kotlin e TypeScript: niente Xcode template, niente Gradle module a mano, niente react-native.config.js. L'app di esempio in /example include già il link al pacchetto via path locale, quindi qualunque modifica ai file nativi si riflette al prossimo npx expo run:ios o run:android.

Scrivere il codice Swift per iOS

Apri ios/ExpoPulseSensorModule.swift. Ogni modulo eredita da Module e implementa definition(), dove dichiari l'API esposta a JS. Ecco un esempio realistico: un modulo che legge la frequenza cardiaca usando HealthKit.

import ExpoModulesCore
import HealthKit

public class ExpoPulseSensorModule: Module {
  private let healthStore = HKHealthStore()

  public func definition() -> ModuleDefinition {
    Name("ExpoPulseSensor")

    Constants([
      "isSupported": HKHealthStore.isHealthDataAvailable()
    ])

    Events("onHeartRateUpdate")

    AsyncFunction("requestPermissions") { () -> [String: Any] in
      let heartRateType = HKQuantityType.quantityType(
        forIdentifier: .heartRate
      )!
      try await healthStore.requestAuthorization(
        toShare: [], read: [heartRateType]
      )
      return ["status": "granted"]
    }

    AsyncFunction("getLastHeartRate") { () -> Double in
      // Lettura del sample più recente da HealthKit
      return try await readLatestHeartRate()
    }

    Function("startMonitoring") {
      self.beginObservingHeartRate()
    }

    Function("stopMonitoring") {
      self.stopObservingHeartRate()
    }
  }

  private func emitHeartRate(_ bpm: Double) {
    sendEvent("onHeartRateUpdate", ["bpm": bpm])
  }
  // beginObservingHeartRate, readLatestHeartRate omessi per brevità
}

Cose da notare: AsyncFunction accetta closure async throws e l'errore viene rigettato sulla Promise JS automaticamente. Function è sincrono e gira sul thread JS — perfetto per operazioni veloci come settare flag interni. Constants espone valori statici disponibili senza chiamata di funzione (utile per capability detection). Events dichiara i nomi degli eventi che il lato JS può sottoscrivere.

Scrivere il codice Kotlin per Android

Il file Kotlin segue la stessa filosofia. Apri android/src/main/java/expo/modules/pulsesensor/ExpoPulseSensorModule.kt e dichiara l'equivalente Android. Il DSL è quasi identico, il che rende la manutenzione cross-platform molto più semplice rispetto al passato.

package expo.modules.pulsesensor

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.Promise
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.records.HeartRateRecord

class ExpoPulseSensorModule : Module() {
  private val context get() = appContext.reactContext!!
  private val client by lazy { HealthConnectClient.getOrCreate(context) }

  override fun definition() = ModuleDefinition {
    Name("ExpoPulseSensor")

    Constants(
      "isSupported" to (HealthConnectClient.getSdkStatus(context) == 1)
    )

    Events("onHeartRateUpdate")

    AsyncFunction("requestPermissions") { promise: Promise ->
      // Lancia un Activity per chiedere il permesso Health Connect
      requestHealthConnectPermission(promise)
    }

    AsyncFunction("getLastHeartRate") Coroutine { ->
      val response = client.readRecords(
        ReadRecordsRequest(
          recordType = HeartRateRecord::class,
          timeRangeFilter = TimeRangeFilter.before(Instant.now())
        )
      )
      response.records.lastOrNull()?.samples?.last()?.beatsPerMinute?.toDouble()
        ?: 0.0
    }

    Function("startMonitoring") { startObserving() }
    Function("stopMonitoring") { stopObserving() }
  }

  private fun emitHeartRate(bpm: Double) {
    sendEvent("onHeartRateUpdate", mapOf("bpm" to bpm))
  }
}

Il keyword Coroutine attiva l'integrazione automatica con Kotlin Coroutines: la funzione gira su un dispatcher non bloccante e l'Expo Modules API la collega alla Promise JS senza dover gestire manualmente i thread o i WritableMap della vecchia Bridge API.

Una volta scritti entrambi i lati, il consumer TypeScript è banale:

// src/ExpoPulseSensorModule.ts
import { NativeModule, requireNativeModule } from 'expo';

declare class ExpoPulseSensorModule extends NativeModule<{
  onHeartRateUpdate: { bpm: number };
}> {
  isSupported: boolean;
  requestPermissions(): Promise<{ status: 'granted' | 'denied' }>;
  getLastHeartRate(): Promise<number>;
  startMonitoring(): void;
  stopMonitoring(): void;
}

export default requireNativeModule<ExpoPulseSensorModule>('ExpoPulseSensor');

Come esporre un view nativo (SwiftUI e Jetpack Compose)

I view nativi sono spesso il motivo principale per scrivere un modulo: pensa a mappe, video player, canvas Skia, viewfinder della camera o widget di sistema. L'Expo Modules API genera un Fabric Component senza che tu debba toccare i file di Codegen.

Lato Swift, dichiari il view dentro la stessa definition():

import SwiftUI
import ExpoModulesCore

class GaugeView: ExpoView {
  private var hostingController: UIHostingController<GaugeContent>!
  let onPress = EventDispatcher()
  private(set) var bpm: Double = 0 {
    didSet { hostingController.rootView = GaugeContent(bpm: bpm) }
  }
  // setup omesso
}

// dentro definition()
View(GaugeView.self) {
  Prop("bpm") { (view: GaugeView, value: Double) in
    view.bpm = value
  }
  Events("onPress")
}

Lato Kotlin si usa ComposeView con lo stesso pattern (View(GaugeView::class) { Prop("bpm") { ... } }). Lato React, il view appare come un normale componente React Native usando requireNativeViewManager, e accetta props con tipizzazione TypeScript ereditata dalle dichiarazioni Swift/Kotlin. Per chi viene dal web, è come scrivere un componente custom con props e onClick — il rendering nativo è dettaglio interno e non si vede dall'esterno.

Eventi, funzioni asincrone e gestione dei permessi

Tre pattern coprono la maggior parte dei casi reali. Gli eventi continui si usano per stream di dati hardware (accelerometro, location, BLE). Lato nativo chiami sendEvent("nome", payload); lato JS usi module.addListener('nome', cb). Le funzioni asincrone sono per qualsiasi operazione I/O o di permission gating, e tornano direttamente una Promise. Le funzioni sincrone esistono ma vanno usate solo per logica veloce su thread JS, altrimenti bloccano l'UI.

Per i permessi, Expo Modules fornisce un'astrazione cross-platform tramite il Permissions Manager. Esempio Swift:

AsyncFunction("getPermissionsAsync") { (promise: Promise) in
  EXPermissions.getPermissionWithPermissionsManager(
    self.appContext?.permissions,
    withRequester: EXLocationRequester.self,
    resolve: promise.resolver, reject: promise.legacyRejecter
  )
}

L'API JS combacia esattamente con expo-location e expo-camera, quindi i consumer non devono imparare nulla di nuovo. Per il debug degli eventi e dei callback nativi consiglio sempre di affiancare React Native DevTools e Hermes Inspector, che mostrano lo stack JS quando una promise rifiutata risale da Swift o Kotlin con un messaggio di errore strutturato.

Pubblicare il modulo su npm e usarlo in produzione

Il modulo è un pacchetto npm standard. Prima della pubblicazione, esegui:

npm run build      # compila il TypeScript in /build
npm run lint
npm run test
npm version 1.0.0
npm publish --access public

L'app consumatrice lo installa con npm install expo-pulse-sensor. Per Expo gestito (Expo Go non funziona con moduli nativi custom) crei una build di sviluppo personalizzata con npx expo prebuild && npx expo run:ios, oppure in cloud con EAS Build (argomento che approfondiamo nella guida a EAS Build e Submit per il deployment). Per Bare React Native, basta cd ios && pod install: Expo Modules autolink fa il resto. Versiona il modulo con SemVer e, se cambi l'API nativa, fai sempre una major release per non rompere le build dei consumatori. Documenta nel CHANGELOG.md eventuali cambi di permessi richiesti, di versione minima iOS/Android o di SDK Expo target.

Errori comuni durante lo sviluppo di moduli nativi

Cinque problemi che vedo ripetersi nei progetti dei clienti:

  1. Dimenticare npx expo prebuild --clean dopo aver modificato expo-module.config.json. La cache del Pod o del Gradle conserva il vecchio binding e ti ritrovi con un UnavailableModuleException a runtime.
  2. Restituire un valore da Function bloccante. Se l'operazione impiega più di 16ms, blocca il thread JS e fa droppare un frame. Usa AsyncFunction per qualsiasi I/O o chiamata di sistema.
  3. Permessi non dichiarati nel config plugin. L'iOS App Review respinge l'app se manca la stringa NSHealthShareUsageDescription. Scrivi un plugin che modifichi Info.plist e AndroidManifest.xml automaticamente.
  4. Tipi Any nelle definizioni. L'Expo Modules API supporta strutture tipizzate: usa Records (Swift) e data class annotate con @Field (Kotlin) invece di mappe generiche per ottenere tipizzazione TS automatica nel consumer.
  5. Non testare con New Architecture attiva. Da RN 0.78 è il default, ma molti CI usano ancora la old arch. Aggiungi una matrix in GitHub Actions che testi entrambi i target finché supporti la legacy.

Risorsa di riferimento ufficiale: la documentazione Expo Modules API contiene una matrix completa dei tipi supportati e esempi di plugin. Per discussioni sui breaking change consulta il changelog del repo expo/expo ad ogni minor release del SDK.

Domande frequenti

Posso usare l'Expo Modules API in un progetto bare React Native senza Expo SDK?

Sì. Basta eseguire npx install-expo-modules@latest nel progetto bare: il comando aggiunge expo-modules-core, configura iOS e Android e attiva l'autolinking. Da quel momento qualunque pacchetto Expo o modulo custom basato su Expo Modules funziona normalmente, senza dover adottare l'intero SDK Expo.

Qual è la differenza tra Function e AsyncFunction?

Function esegue sincronicamente sul thread JS e ritorna direttamente un valore: utile solo per operazioni in memoria che durano meno di un frame. AsyncFunction ritorna una Promise e gira su un dispatcher di sfondo, quindi è la scelta corretta per I/O, networking, accesso al disco e qualunque API piattaforma con callback nativa.

L'Expo Modules API supporta la Nuova Architettura React Native?

Sì, è il caso d'uso primario. Il framework genera codice TurboModule e Fabric Component automaticamente sotto la New Architecture, mentre fornisce un layer di interop per progetti ancora sulla bridge legacy. Da React Native 0.78 (gennaio 2026) la New Arch è il default in ogni nuovo progetto.

Posso pubblicare un modulo Expo che includa un SDK iOS via Swift Package Manager?

Sì. Aggiungi la dipendenza in ExpoModulesPulseSensor.podspec usando s.dependency per CocoaPods, oppure dichiara un Package.swift se preferisci SPM diretto. L'autolinking di Expo Modules legge entrambi i formati e li propaga al consumer alla fase di pod install.

Devo scrivere un Config Plugin anche per un modulo semplice?

Solo se il modulo richiede modifiche al progetto nativo (chiavi Info.plist, permessi Android, capabilities Xcode, schemi URL). Per moduli puramente JS-to-native senza side-effect sul progetto non serve nessun plugin: l'autolinking di Expo Modules basta.

Anita Iyer
Sull'Autore Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.