Summary
The @react-native-runtimes/core package currently exposes two parallel native APIs to JavaScript:
- Nitro HybridObject (
ThreadedRuntimeFunctions) — partial migration for cross-runtime function calls (install, run)
- Legacy bridge Native Module (
NativeModules.ThreadedRuntime) — classic RCTBridgeModule (iOS) / ReactContextBaseJavaModule (Android) for all lifecycle operations and a fallback runtime-function path
This is confusing, duplicates code paths (especially for ThreadedRuntime.run()), and blocks making react-native-nitro-modules a hard requirement. We should remove the bridge module entirely and consolidate everything into one Nitro HybridObject named ThreadedRuntime.
Note: The public ThreadedRuntime module is not a TurboModule — there is no codegen spec. Internal TurboModule delegate code (ThreadedRuntimeTurboModuleDelegate, DefaultTurboModuleManagerDelegate) is separate infrastructure for secondary RN runtimes and should stay.
Current state
Nitro (partial — keep and extend)
| Piece |
Role |
src/specs/ThreadedRuntimeFunctions.nitro.ts |
Nitrogen spec |
cpp/HybridThreadedRuntimeFunctions.{hpp,cpp} |
C++ implementation |
nitrogen/generated/** |
Generated registration/spec glue |
nitro.json → "ThreadedRuntimeFunctions" |
Autolinking config |
JS access: NitroModules.createHybridObject('ThreadedRuntimeFunctions')
Methods:
install(runtimeName?) — raw JSI; installs __rnrRegisterRuntimeFunction / __rnrCallRuntimeFunction and registers a Nitro Dispatcher
run(runtimeName, functionId, argsJson) — cross-runtime calls via RuntimeFunctionScheduler (C++ JSI, no bridge round-trip)
Legacy bridge module (remove)
| Platform |
Bridge entry |
Core implementation |
| iOS |
ThreadedRuntime implements RCTBridgeModule with RCT_EXPORT_METHOD(...) in ThreadedRuntime.mm |
Same file's + (void) static methods |
| Android |
ThreadedRuntimeBridgeModule.kt (@ReactModule(name = "ThreadedRuntime")) |
ThreadedRuntime.kt object |
JS access: NativeModules.ThreadedRuntime in packages/core/src/ThreadedRuntime.tsx
Methods:
prewarmRuntime / preloadRuntime / prewarmRuntimeWithOptions
runHeadlessTask / dispatchHeadlessTask
callRuntimeFunction ← legacy fallback for ThreadedRuntime.run()
completeRuntimeFunctionCall ← callback leg of legacy path only
destroyRuntime / destroyAllRuntimes / getRuntimeNames
addListener / removeListeners (no-ops for NativeEventEmitter compat)
Out of scope (keep as-is for this refactor)
ThreadedRuntimeSurface — Fabric view via ViewGroupManager / requireNativeComponent
ThreadedRuntimeTurboModuleDelegate (iOS) / DefaultTurboModuleManagerDelegate (Android) — internal TurboModule resolution for secondary runtimes
@react-native-runtimes/state still has its own legacy SharedZustandStoreModule — separate follow-up
Dual runtime-function paths (why legacy code is bulky)
Nitro path (target):
JS ThreadedRuntime.run()
→ HybridThreadedRuntimeFunctions.run()
→ RuntimeFunctionScheduler (C++ dispatcher)
→ __rnrCallRuntimeFunction in target runtime (JSI)
Legacy bridge path (delete):
JS ThreadedRuntime.run()
→ NativeModules.callRuntimeFunction (Promise + callId)
→ native queue + flushRuntimeFunctionCalls
→ host.callFunctionOnJSModule("ThreadedRuntimeFunctionRunner", "run", ...)
→ JS runRegisteredRuntimeFunction()
→ NativeModules.completeRuntimeFunctionCall(callId, ...)
Legacy path adds substantial code on both platforms:
- iOS:
callRuntimeFunctionWithRuntimeName, completeRuntimeFunctionCallWithCallId, flushRuntimeFunctionCallsWithRuntimeName, promise maps
- Android:
pendingRuntimeFunctionCalls, pendingRuntimeFunctionPromises, invokeRuntimeFunctionCall, completeRuntimeFunctionCall
- JS:
runRegisteredRuntimeFunction, completeRuntimeFunctionCall, registerCallableModule(RUNTIME_FUNCTION_RUNNER_MODULE, ...)
Target state: one HybridObject ThreadedRuntime
Single Nitro entry point:
NitroModules.createHybridObject<ThreadedRuntime>('ThreadedRuntime')
Spec (src/specs/ThreadedRuntime.nitro.ts)
Merge existing functions spec with lifecycle methods. Drop bridge-only aliases.
import type { HybridObject } from 'react-native-nitro-modules';
export interface ThreadedRuntime
extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
// Cross-runtime functions (from ThreadedRuntimeFunctions)
run(
runtimeName: string,
functionId: string,
argsJson: string,
): Promise<string>;
// Lifecycle (from bridge module)
prewarmRuntime(runtimeName: string): Promise<void>;
prewarmRuntimeWithOptions(
runtimeName: string,
kind: string,
useMainNativeModules: boolean,
): Promise<void>;
runHeadlessTask(
runtimeName: string,
taskName: string,
payloadJson: string,
): Promise<void>;
destroyRuntime(runtimeName: string): Promise<void>;
destroyAllRuntimes(): Promise<void>;
getRuntimeNames(): Promise<string[]>;
}
install(runtimeName?) stays out of the .nitro.ts file — registered as a raw JSI hybrid method in C++ via registerRawHybridMethod (same pattern as today). TypeScript adds it on the cached hybrid type:
type ThreadedRuntimeHybrid = ThreadedRuntime & {
install: (runtimeName?: string) => void;
};
nitro.json
"autolinking": {
"ThreadedRuntime": {
"all": {
"language": "c++",
"implementationClassName": "HybridThreadedRuntime"
}
}
}
Remove the ThreadedRuntimeFunctions entry. Run npm run specs in packages/core to regenerate nitrogen output.
Native implementation: one C++ class on both platforms
Use C++ everywhere for the hybrid object. Lifecycle ops delegate to existing platform code through dispatcher shims.
| Method |
Implementation |
install |
RuntimeFunctionJsi + Nitro Dispatcher registration (existing) |
run |
RuntimeFunctionScheduler::callRuntimeFunctionOnRuntime (existing) |
prewarmRuntime* |
ThreadedRuntimeDispatcher → [ThreadedRuntime prewarm…] / ThreadedRuntime.prewarmRuntimeWithOptions |
runHeadlessTask |
ThreadedRuntimeDispatcher::dispatchHeadlessTask (existing) |
destroyRuntime / destroyAllRuntimes / getRuntimeNames |
Extend ThreadedRuntimeDispatcher.h with JNI (Android) + .mm (iOS) wrappers |
Rename/merge: HybridThreadedRuntimeFunctions → HybridThreadedRuntime (one .hpp/.cpp).
Android Context: extend ThreadedRuntimeDispatcher.h JNI helpers to accept/cache Application context (set once from NativeComposeThreadedRuntimeOnLoad / OnLoad.cpp, replacing init currently in ThreadedRuntimeBridgeModule.init).
iOS: C++ calls ObjC static methods via ThreadedRuntimeDispatcher.mm (no bridge context needed).
JS changes (packages/core/src/ThreadedRuntime.tsx)
- Add
getThreadedRuntimeNitro() — lazy cache, fail/warn loudly on native platforms if Nitro unavailable
- Replace all
nativeRuntime?.… calls with Nitro hybrid methods
- Remove
NativeModules import and ThreadedRuntimeNativeModule type
- Remove legacy runtime-function fallback in
ThreadedRuntime.run()
- Remove
completeRuntimeFunctionCall, runRegisteredRuntimeFunction, and RUNTIME_FUNCTION_RUNNER_MODULE callable module registration
Keep:
registerCallableModule(HEADLESS_TASK_RUNNER_MODULE, …) — headless dispatch still uses callFunctionOnJSModule, not the bridge
Make react-native-nitro-modules a hard peer dependency (already listed; today JS treats it as optional).
Delete
| Layer |
Remove |
| Android |
ThreadedRuntimeBridgeModule.kt |
| Android |
Legacy fn path in ThreadedRuntime.kt: callRuntimeFunction, completeRuntimeFunctionCall, pending queues, flushRuntimeFunctionCalls, invokeRuntimeFunctionCall |
| iOS |
RCTBridgeModule conformance, RCT_EXPORT_MODULE, all RCT_EXPORT_METHOD |
| iOS |
Legacy fn path: callRuntimeFunctionWithRuntimeName, completeRuntimeFunctionCallWithCallId, flushRuntimeFunctionCallsWithRuntimeName, promise maps |
| Spec |
src/specs/ThreadedRuntimeFunctions.nitro.ts |
| C++ |
HybridThreadedRuntimeFunctions.{hpp,cpp} (merged into HybridThreadedRuntime) |
| Package |
ThreadedRuntimePackage.createNativeModules() — package registers ViewManager only |
Keep unchanged
ThreadedRuntime.kt / ThreadedRuntime.mm host orchestration (minus bridge exports and legacy fn queue)
ThreadedRuntimeSurfaceManager / surface views
RuntimeFunctionJsi.*, RuntimeFunctionScheduler.*
- TurboModule delegates inside secondary runtimes
Suggested file map after refactor
packages/core/
├── src/specs/
│ └── ThreadedRuntime.nitro.ts # single spec (replaces ThreadedRuntimeFunctions)
├── cpp/
│ ├── HybridThreadedRuntime.{hpp,cpp} # merged hybrid
│ └── nativecompose/threadedruntime/ # JSI + scheduler + dispatcher (extended)
├── android/.../
│ ├── ThreadedRuntime.kt # core engine (trim legacy fn path)
│ ├── ThreadedRuntimePackage.kt # ViewManager only
│ └── ThreadedRuntimeSurface*.kt
├── ios/
│ ├── ThreadedRuntime.{h,mm} # engine only, no RCTBridgeModule
│ ├── ThreadedRuntimeDispatcher.mm # extended C++ shim
│ └── ThreadedRuntimeSurface*.mm
└── nitrogen/generated/ # regenerated
Execution order
- Extend dispatcher — add
destroyRuntime, destroyAllRuntimes, getRuntimeNames (+ Android context init) to ThreadedRuntimeDispatcher
- New spec + codegen —
ThreadedRuntime.nitro.ts, update nitro.json, merge C++ hybrid class, delete old functions spec/class
- Wire JS — single
getThreadedRuntimeNitro(), switch all call sites, remove legacy fn runner
- Delete bridge — bridge module, RCT exports, legacy fn queue machinery
- Trim package —
ThreadedRuntimePackage view-only; update README; verify example app
Test plan
After each phase, verify on iOS and Android:
Risks / breaking changes
- Breaking: Removing
NativeModules.ThreadedRuntime breaks any direct consumer outside this package. Within the monorepo, only ThreadedRuntime.tsx uses it today.
- Android Context — confirm Nitro init timing vs first
prewarm call; may need Application context holder set at startup.
- Surface view — remains old-style
ViewManager; Nitro Views are a separate future effort.
- State package —
@react-native-runtimes/state still has dual Nitro + bridge API; not blocking this work but should be tracked separately.
References
- Nitro spec (current):
packages/core/src/specs/ThreadedRuntimeFunctions.nitro.ts
- JS dual-path usage:
packages/core/src/ThreadedRuntime.tsx (getRuntimeFunctionsNitro() + nativeRuntime)
- Bridge module (Android):
packages/core/android/.../ThreadedRuntimeBridgeModule.kt
- Bridge module (iOS):
packages/core/ios/ThreadedRuntime.mm (RCT_EXPORT_METHOD)
- Prior art (Nitro-first, but still has bridge):
packages/state/src/index.ts
Summary
The
@react-native-runtimes/corepackage currently exposes two parallel native APIs to JavaScript:ThreadedRuntimeFunctions) — partial migration for cross-runtime function calls (install,run)NativeModules.ThreadedRuntime) — classicRCTBridgeModule(iOS) /ReactContextBaseJavaModule(Android) for all lifecycle operations and a fallback runtime-function pathThis is confusing, duplicates code paths (especially for
ThreadedRuntime.run()), and blocks makingreact-native-nitro-modulesa hard requirement. We should remove the bridge module entirely and consolidate everything into one Nitro HybridObject namedThreadedRuntime.Current state
Nitro (partial — keep and extend)
src/specs/ThreadedRuntimeFunctions.nitro.tscpp/HybridThreadedRuntimeFunctions.{hpp,cpp}nitrogen/generated/**nitro.json→"ThreadedRuntimeFunctions"JS access:
NitroModules.createHybridObject('ThreadedRuntimeFunctions')Methods:
install(runtimeName?)— raw JSI; installs__rnrRegisterRuntimeFunction/__rnrCallRuntimeFunctionand registers a NitroDispatcherrun(runtimeName, functionId, argsJson)— cross-runtime calls viaRuntimeFunctionScheduler(C++ JSI, no bridge round-trip)Legacy bridge module (remove)
ThreadedRuntimeimplementsRCTBridgeModulewithRCT_EXPORT_METHOD(...)inThreadedRuntime.mm+ (void)static methodsThreadedRuntimeBridgeModule.kt(@ReactModule(name = "ThreadedRuntime"))ThreadedRuntime.ktobjectJS access:
NativeModules.ThreadedRuntimeinpackages/core/src/ThreadedRuntime.tsxMethods:
prewarmRuntime/preloadRuntime/prewarmRuntimeWithOptionsrunHeadlessTask/dispatchHeadlessTaskcallRuntimeFunction← legacy fallback forThreadedRuntime.run()completeRuntimeFunctionCall← callback leg of legacy path onlydestroyRuntime/destroyAllRuntimes/getRuntimeNamesaddListener/removeListeners(no-ops forNativeEventEmittercompat)Out of scope (keep as-is for this refactor)
ThreadedRuntimeSurface— Fabric view viaViewGroupManager/requireNativeComponentThreadedRuntimeTurboModuleDelegate(iOS) /DefaultTurboModuleManagerDelegate(Android) — internal TurboModule resolution for secondary runtimes@react-native-runtimes/statestill has its own legacySharedZustandStoreModule— separate follow-upDual runtime-function paths (why legacy code is bulky)
Nitro path (target):
Legacy bridge path (delete):
Legacy path adds substantial code on both platforms:
callRuntimeFunctionWithRuntimeName,completeRuntimeFunctionCallWithCallId,flushRuntimeFunctionCallsWithRuntimeName, promise mapspendingRuntimeFunctionCalls,pendingRuntimeFunctionPromises,invokeRuntimeFunctionCall,completeRuntimeFunctionCallrunRegisteredRuntimeFunction,completeRuntimeFunctionCall,registerCallableModule(RUNTIME_FUNCTION_RUNNER_MODULE, ...)Target state: one HybridObject
ThreadedRuntimeSingle Nitro entry point:
Spec (
src/specs/ThreadedRuntime.nitro.ts)Merge existing functions spec with lifecycle methods. Drop bridge-only aliases.
install(runtimeName?)stays out of the.nitro.tsfile — registered as a raw JSI hybrid method in C++ viaregisterRawHybridMethod(same pattern as today). TypeScript adds it on the cached hybrid type:nitro.jsonRemove the
ThreadedRuntimeFunctionsentry. Runnpm run specsinpackages/coreto regenerate nitrogen output.Native implementation: one C++ class on both platforms
Use C++ everywhere for the hybrid object. Lifecycle ops delegate to existing platform code through dispatcher shims.
installRuntimeFunctionJsi+ NitroDispatcherregistration (existing)runRuntimeFunctionScheduler::callRuntimeFunctionOnRuntime(existing)prewarmRuntime*ThreadedRuntimeDispatcher→[ThreadedRuntime prewarm…]/ThreadedRuntime.prewarmRuntimeWithOptionsrunHeadlessTaskThreadedRuntimeDispatcher::dispatchHeadlessTask(existing)destroyRuntime/destroyAllRuntimes/getRuntimeNamesThreadedRuntimeDispatcher.hwith JNI (Android) +.mm(iOS) wrappersRename/merge:
HybridThreadedRuntimeFunctions→HybridThreadedRuntime(one.hpp/.cpp).Android Context: extend
ThreadedRuntimeDispatcher.hJNI helpers to accept/cacheApplicationcontext (set once fromNativeComposeThreadedRuntimeOnLoad/OnLoad.cpp, replacing init currently inThreadedRuntimeBridgeModule.init).iOS: C++ calls ObjC static methods via
ThreadedRuntimeDispatcher.mm(no bridge context needed).JS changes (
packages/core/src/ThreadedRuntime.tsx)getThreadedRuntimeNitro()— lazy cache, fail/warn loudly on native platforms if Nitro unavailablenativeRuntime?.…calls with Nitro hybrid methodsNativeModulesimport andThreadedRuntimeNativeModuletypeThreadedRuntime.run()completeRuntimeFunctionCall,runRegisteredRuntimeFunction, andRUNTIME_FUNCTION_RUNNER_MODULEcallable module registrationKeep:
registerCallableModule(HEADLESS_TASK_RUNNER_MODULE, …)— headless dispatch still usescallFunctionOnJSModule, not the bridgeMake
react-native-nitro-modulesa hard peer dependency (already listed; today JS treats it as optional).Delete
ThreadedRuntimeBridgeModule.ktThreadedRuntime.kt:callRuntimeFunction,completeRuntimeFunctionCall, pending queues,flushRuntimeFunctionCalls,invokeRuntimeFunctionCallRCTBridgeModuleconformance,RCT_EXPORT_MODULE, allRCT_EXPORT_METHODcallRuntimeFunctionWithRuntimeName,completeRuntimeFunctionCallWithCallId,flushRuntimeFunctionCallsWithRuntimeName, promise mapssrc/specs/ThreadedRuntimeFunctions.nitro.tsHybridThreadedRuntimeFunctions.{hpp,cpp}(merged intoHybridThreadedRuntime)ThreadedRuntimePackage.createNativeModules()— package registers ViewManager onlyKeep unchanged
ThreadedRuntime.kt/ThreadedRuntime.mmhost orchestration (minus bridge exports and legacy fn queue)ThreadedRuntimeSurfaceManager/ surface viewsRuntimeFunctionJsi.*,RuntimeFunctionScheduler.*Suggested file map after refactor
Execution order
destroyRuntime,destroyAllRuntimes,getRuntimeNames(+ Android context init) toThreadedRuntimeDispatcherThreadedRuntime.nitro.ts, updatenitro.json, merge C++ hybrid class, delete old functions spec/classgetThreadedRuntimeNitro(), switch all call sites, remove legacy fn runnerThreadedRuntimePackageview-only; update README; verify example appTest plan
After each phase, verify on iOS and Android:
ThreadedRuntime.prewarm()/prewarmBusinessRuntime()ThreadedRuntimeSurface/ threaded componentThreadedRuntime.runHeadlessTask()runtimeFunction(...).runOn(otherRuntime, ...)cross-runtime call (requiresinstall()in both runtimes)ThreadedRuntime.destroy()/destroyAll()/getRuntimeNames()example/) still runs withreact-native-nitro-moduleslinkedRisks / breaking changes
NativeModules.ThreadedRuntimebreaks any direct consumer outside this package. Within the monorepo, onlyThreadedRuntime.tsxuses it today.prewarmcall; may needApplicationcontext holder set at startup.ViewManager; Nitro Views are a separate future effort.@react-native-runtimes/statestill has dual Nitro + bridge API; not blocking this work but should be tracked separately.References
packages/core/src/specs/ThreadedRuntimeFunctions.nitro.tspackages/core/src/ThreadedRuntime.tsx(getRuntimeFunctionsNitro()+nativeRuntime)packages/core/android/.../ThreadedRuntimeBridgeModule.ktpackages/core/ios/ThreadedRuntime.mm(RCT_EXPORT_METHOD)packages/state/src/index.ts