Skip to content

feat(react-native-host): expose auxiliary RCTTurboModuleManagerDelegate via RNXHostConfig (2/3)#4145

Draft
Saadnajmi wants to merge 2 commits intomicrosoft:mainfrom
Saadnajmi:feat-auxiliary-tm-manager-delegate
Draft

feat(react-native-host): expose auxiliary RCTTurboModuleManagerDelegate via RNXHostConfig (2/3)#4145
Saadnajmi wants to merge 2 commits intomicrosoft:mainfrom
Saadnajmi:feat-auxiliary-tm-manager-delegate

Conversation

@Saadnajmi
Copy link
Copy Markdown
Contributor

Description

Depends on #4144

Adds an optional turboModuleManagerDelegate property to RNXHostConfig that lets consumers participate in TurboModule construction without replacing RNXTurboModuleAdapter.

Motivation

RNXTurboModuleAdapter is rnx-kit's RCTTurboModuleManagerDelegate. Today it owns the full TurboModule construction path:

  • getModuleClassFromName: falls back to RCTCoreModulesClassProvider
  • getModuleInstanceFromClass: falls back to RCTAppSetupDefaultModuleFromClass

For modules that need per-host state — e.g., an RCTExceptionsManager initialized with a per-host delegate, or an RCTDevSettings with a custom data source — there's no extension
point. In bridge mode this case is handled via RCTBridgeDelegate.extraModulesForBridge: (the bridge prefers pre-constructed instances vended there). In bridgeless mode, that hook
isn't called — modules go through RCTTurboModuleManager.delegate instead.

Design

RNXHostConfig adds:

@property (nonatomic, readonly, weak, nullable) id<RCTTurboModuleManagerDelegate> turboModuleManagerDelegate;

RNXTurboModuleAdapter adds a hostConfig weak reference and, for each RCTTurboModuleManagerDelegate method it implements, consults the auxiliary via respondsToSelector: first;
returning nil / Nil falls through to the existing default behavior.

- (Class)getModuleClassFromName:(char const *)name {
    id<RCTTurboModuleManagerDelegate> aux = _hostConfig.turboModuleManagerDelegate;
    if ([aux respondsToSelector:_cmd]) {
        Class cls = [aux getModuleClassFromName:name];
        if (cls != Nil) return cls;
    }
    return RCTCoreModulesClassProvider(name);
}

Same shape for getModuleInstanceFromClass:.

Properties

  • Symmetric with bridge-mode extraModulesForBridge:: the per-host injection point that bridge consumers use.
  • Backward-compatible: consumers that don't implement turboModuleManagerDelegate see no behavior change.
  • Generic, not per-method: any current or future RCTTurboModuleManagerDelegate method (getModuleProvider:, getTurboModule:jsInvoker:, etc.) the consumer implements is routed
    automatically — no per-hook plumbing required in rnx-kit.
  • Zero overhead when the auxiliary delegate doesn't respond — single respondsToSelector: check.

Example

@interface MyHostConfig : NSObject <RNXHostConfig, RCTTurboModuleManagerDelegate>
@end

@implementation MyHostConfig {
    MyExceptionsManagerDelegate *_exceptionsDelegate;
}

- (id<RCTTurboModuleManagerDelegate>)turboModuleManagerDelegate {
    return self;
}

// Required RCTTurboModuleManagerDelegate methods — return nil to defer to rnx-kit's defaults.
- (Class)getModuleClassFromName:(const char *)name { return Nil; }

- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)cls {
    if (cls == [RCTExceptionsManager class]) {
        return [[RCTExceptionsManager alloc] initWithDelegate:_exceptionsDelegate];
    }
    return nil;
}
@end

Test plan

Tested internally

Saadnajmi added 2 commits May 10, 2026 17:30
Adds three optional lifecycle hooks to RNXHostConfig. All three are
opt-in; consumers that don't implement them see no behavior change.

* host:didLoadInstanceWithError: and hostWillUnloadInstance:
  ReactNativeHost subscribes to RCTJavaScriptDidLoad,
  RCTJavaScriptDidFailToLoad, and RCTBridgeWillBeInvalidated
  notifications and forwards to the config when the corresponding
  selectors are implemented. dealloc removes observers.

* host:didInitializeRuntime: (Objective-C++ only) fires inside the
  bridgeless runtime-init lambda, after host bindings install but
  before the user JS bundle loads. Useful for loading pre-user JS
  (e.g. platform bundles) via runtime.evaluateJavaScript before the
  app bundle runs. Wired via an internal _RNXForwardingRCTHostDelegate
  passed as RCTHost's hostDelegate (was nil); retained as an ivar
  because RCTHost stores host delegates weakly.
…te via RNXHostConfig

Add an optional `turboModuleManagerDelegate` property to `RNXHostConfig`
that lets the consumer participate in TurboModule construction without
replacing `RNXTurboModuleAdapter`. The adapter consults it via
`respondsToSelector:` before its existing defaults; returning `Nil` /
`nil` falls through to the previous behavior
(`RCTCoreModulesClassProvider`, `RCTAppSetupDefaultModuleFromClass` /
`[cls new]`).

The shape mirrors how bridge mode lets consumers vend pre-constructed
modules via `RCTBridgeDelegate.extraModulesForBridge:` -- a per-host
injection point for modules that carry consumer-side state (e.g.,
`RCTExceptionsManager` with a custom delegate, `RCTDevSettings` with a
custom data source).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant