Branchement d’un module Turbo intégré de bout en bout avec React Native Codegen
Définir une spécification typée, exécuter le codegen, et implémenter un module Turbo sur iOS et Android en utilisant des méthodes synchrones, Promise, callback ainsi que des émetteurs d’événements.
L nouvelle architecture de React Native remplace le pont asynchrone par des Turbo Modules, que JavaScript appelle via JSI grâce à des bindings générés et typés. Cela permet de réduire la charge liée aux appels ainsi que d’appliquer des contrôles vérifiés par codegen plutôt que par convention. Cette étape vous montrera comment créer un module au sein d’une application (sans package npm) et abordera les trois styles d’appels que vous utiliserez en pratique : une valeur de retour synchrone, des résultats asynchrones via une Promise ou un callback, et un flux d’événements.
Rédaction de la spécification typée
Créez un dossier specs à la racine du projet pour y stocker les schémas des modules :
/your-app
/specs
Dans ce fichier, ajoutez NativeShowCase.ts. Codegen ne prend en compte que les fichiers de spécification dont le nom commence par Native. La spécification ci-dessous définit une méthode par style : haveCameraFlash renvoie de manière synchrone, toggleFlashLight renvoie une Promise, fetchUser accepte un callback, et evenListenerCalled est un émetteur d’événements transportant des paires clé/valeur. getEnforcing lance une exception au démarrage si la partie native est manquante, ce qui permet de détecter tôt d’éventuelles erreurs de connexion. Les émetteurs d’événements typés dans les spécifications sont une fonctionnalité récente, il convient donc de vérifier que votre version de React Native prend en charge CodegenTypes.EventEmitter.
import type { CodegenTypes, TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
export type KeyValuePair = {
key: string;
value: string;
};
export interface Spec extends TurboModule {
toggleFlashLight(): Promise<boolean>;
haveCameraFlash(): boolean;
fetchUser(
id: string,
callback: (user: { id: string; name: string }) => void
): void;
readonly evenListenerCalled: CodegenTypes.EventEmitter<KeyValuePair>;
}
export default TurboModuleRegistry.getEnforcing<Spec>("NativeShowCase");
Ce fichier constitue la seule source de vérité ; les bindings des deux plateformes sont générés à partir de lui.
Configuration et exécution de Codegen
Ajoutez un bloc codegenConfig dans package.json. jsSrcsDir doit correspondre à votre dossier specs, et javaPackageName détermine l’endroit où les classes Android seront générées :
"codegenConfig": {
"name": "NativeShowCase",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativeshowcase"
}
}
Ensuite, générez les artefacts. Sur Android, exécutez la tâche Gradle :
cd android && ./gradlew generateCodegenArtifactsFromSchema
Sur iOS, codegen s’exécute lors de l’installation des pods :
cd ios && pod install
Vous disposez maintenant d’interfaces, de en-têtes et de code de liaison générés pour les deux plateformes.
Mise en œuvre du module sur iOS
Ouvrez le .xcworkspace dans Xcode, créez un groupe NativeShowCase, et ajoutez une classe Objective-C nommée RCTNativeShowCase. Renommez le fichier .m en .mm : les Turbo Modules ont besoin d’Objective-C++ pour interagir avec la couche JSI en C++.
L’en-tête importe le module généré et hérite de la classe de base générée tout en adoptant le protocole généré :
#import <Foundation/Foundation.h>
#import <NativeShowCase/NativeShowCase.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeShowCase : NativeShowCaseSpecBase<NativeShowCaseSpec>
@end
NS_ASSUME_NONNULL_END
L’implémentation renvoie le nom du module, transmet à React Native un objet JSI provenant de getTurboModule, et met en œuvre chaque méthode spécifiée. toggleFlashLight résout la Promise, fetchUser appelle la fonction de rappel, et haveCameraFlash renvoie immédiatement mais démarre un compte à rebours qui émet dix événements via la méthode générée emitEvenListenerCalled.
#import "RCTNativeShowCase.h"
@interface RCTNativeShowCase()
@property (nonatomic, strong) NSTimer *myTimer;
@property (nonatomic, assign) int count;
@end
@implementation RCTNativeShowCase
+ (NSString *)moduleName {
return @"NativeShowCase";
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeShowCaseSpecJSI>(params);
}
- (void)toggleFlashLight:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
resolve(@(false));
}
- (nonnull NSNumber *)haveCameraFlash {
[self startTimer];
return [NSNumber numberWithBool:true];
}
- (void)fetchUser:(nonnull NSString *)userId callback:(nonnull RCTResponseSenderBlock)callback {
callback(@[@{@"key": userId, @"value": @"John"}]);
}
-(void)startTimer {
if(self.myTimer) {
[self stopTimer];
}
self.count = 0;
self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateCount)
userInfo:nil
repeats:YES];
}
- (void)updateCount {
self.count += 1;
[self emitEvenListenerCalled:@{@"key": @"count", @"value": @(_count)}];
if (self.count == 10) {
[self stopTimer];
}
}
- (void)stopTimer {
[self.myTimer invalidate];
self.myTimer = nil;
}
@end
Notez que cette version iOS de fetchUser envoie des champs key/value, tandis que les spécifications prévoient id et name ; alignez-les dans le code réel, car codegen ne valide pas la structure des données de la fonction de rappel en temps de exécution.
Mise en œuvre du module sur Android
Créer le package com.nativeshowcase déclaré dans codegenConfig. La classe du module étend la classe générée NativeShowCaseSpec et surcharge chaque méthode : elle active/désactive un drapeau et résout la Promise, exécute un CountDownTimer qui émet le nombre de secondes restantes à chaque tick, et crée un tableau pour la fonction de rappel.
package com.nativeshowcase
import android.os.CountDownTimer
import android.widget.Toast
import com.facebook.fbreact.specs.NativeShowCaseSpec
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
class NativeShowCaseModule(reactContext: ReactApplicationContext) :
NativeShowCaseSpec(reactContext) {
var timer: CountDownTimer? = null
var lightOn = false
override fun toggleFlashLight(promise: Promise?) {
Toast.makeText(reactApplicationContext, "Let's turn on flash light", Toast.LENGTH_LONG)
.show()
lightOn = !lightOn
promise?.resolve(lightOn)
}
override fun haveCameraFlash(): Boolean {
if (timer != null) {
timer?.cancel()
timer = null
}
timer = object : CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val eventData = Arguments.createMap().apply {
putString("key", "count")
putInt("value", (millisUntilFinished / 1000).toInt())
}
emitEvenListenerCalled(eventData)
}
override fun onFinish() {
timer?.cancel()
timer = null
}
}
timer?.start()
return true
}
override fun fetchUser(id: String?, callback: Callback?) {
val eventData = Arguments.createMap().apply {
putString("id", id)
putString("name", "John")
}
callback.let { it?.invoke(eventData) }
}
companion object {
const val NAME = "NativeShowCase"
}
override fun getName() = NAME
}
Une classe de package basée sur BaseReactPackage indique à React Native comment instancier le module. Le drapeau isTurboModule = true présent dans son ReactModuleInfo est ce qui le fait passer par le nouveau système.
package com.nativeshowcase
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
class NativeShowCasePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
if (name == NativeShowCaseModule.NAME) return NativeShowCaseModule(reactContext)
return null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeShowCaseModule.NAME to ReactModuleInfo(
name = NativeShowCaseModule.NAME,
className = NativeShowCaseModule.NAME,
canOverrideExistingModule = false,
needsEagerInit = true,
isCxxModule = false,
isTurboModule = true
)
)
}
}
Finalement, enregistrer le package dans MainApplication.kt en l’ajoutant à la liste des packages auto-liés :
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
add(NativeShowCasePackage())
},
)
}
override fun onCreate() {
super.onCreate()
loadReactNative(this)
}
}
Appel du module depuis React
Importez l’export par défaut de la spécification et appelez-le comme n’importe quel objet. L’effet s’abonne au générateur d’événements et retourne une fonction de nettoyage qui supprime l’écouteur en cas de désinstallation ; un bouton appelle la méthode synchrone, l’autre utilise les méthodes Promise et callback.
import { StatusBar, StyleSheet, useColorScheme, View, Button } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import NativeShowCase from './specs/NativeShowCase';
import { useEffect } from 'react';
function App() {
const isDarkMode = useColorScheme() === 'dark';
return (
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<AppContent />
</SafeAreaProvider>
);
}
function AppContent() {
useEffect(() => {
const listener = NativeShowCase.evenListenerCalled(pair => {
console.log('Event received:', pair);
});
return () => listener.remove();
}, []);
return (
<View style={styles.container}>
<Button
title="Have Camera Flash?"
onPress={() => {
console.log('Have Camera Flash:', NativeShowCase.haveCameraFlash());
}}
/>
<Button
title="Toggle Flashlight"
onPress={() => {
NativeShowCase.toggleFlashLight()
.then(isOn => console.log('Flashlight:', isOn))
.catch(err => console.error(err));
NativeShowCase.fetchUser('123', user => {
console.log('Fetched user:', user);
});
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
});
export default App;
Utilisez les méthodes synchrones avec modération : elles bloquent le thread JavaScript jusqu’au retour de la fonction native, donc réservez-les aux recherches peu coûteuses. Pour une approche alternative de liaison, consultez comment Nitro Modules se compare à Turbo Modules.
Points clés
- La spécification TypeScript constitue le contrat ; codegen en dérive des interfaces natives pour les deux plateformes.
- Nommez les fichiers de spécification avec le préfixe
Nativeet maintenezcodegenConfigen synchronisation avec les dossiers et les packages.
.mm ; les paquets Android doivent marquer les modules avec isTurboModule.Lectures complémentaires
- Gestion des notifications push et des liens profonds dans React Native — Configuration de liens universels et de liens d’application, intégration de Pusher Beams dans une application React Native, et mise en correspondance des URL web avec les écrans natifs afin qu’un clic sur une notification ouvre bien la bonne page.
- Planifier une mise à jour du Expo SDK 58 : iOS 27, React Native 0.88 et nouveaux outils — Une présentation pratique de la version bêta du Expo SDK 58 : quels changements pour iOS 27 et React Native 0.88, quelles fonctionnalités sont expérimentales, et comment tester la mise à jour en toute sécurité.
- Traiter les dossiers natifs comme résultats de construction avec Expo Prebuild et CNG — Comment la génération native continue permet à une application Expo d’utiliser des modules natifs personnalisés, des plugins de configuration et des secrets EAS sans avoir à enregistrer ou modifier manuellement les dossiers iOS et Android.
- Notifications React Native : permissions, canaux et cycle de vie de FCM — Découvrez comment les permissions, les canaux Android, les tokens FCM ainsi que les gestionnaires d’état en premier plan, en arrière-plan et lors de la fermeture s’intègrent dans un système de notifications React Native avec Notifee.
- Mises à jour OTA auto-hébergées pour React Native base avec hot-updater et Supabase — Mettez en place des mises à jour JavaScript sans connexion dans une application React Native base grâce à hot-updater et Supabase : initialisation, hook de mise à jour silencieuse, canaux, scripts de déploiement et fonction de réversion.