Skip to content

feat(react-native-host): allow opting out of React-RCTAppDelegate dependency (3/3)#4146

Draft
Saadnajmi wants to merge 3 commits intomicrosoft:mainfrom
Saadnajmi:feat-opt-out-rct-app-delegate
Draft

feat(react-native-host): allow opting out of React-RCTAppDelegate dependency (3/3)#4146
Saadnajmi wants to merge 3 commits intomicrosoft:mainfrom
Saadnajmi:feat-opt-out-rct-app-delegate

Conversation

@Saadnajmi
Copy link
Copy Markdown
Contributor

Description

Depends on #4144
Depends on #4145

Adds RNX_USE_RCT_APP_DELEGATE_HELPERS (default 1, preserving current behavior). Set to 0 to drop the React-RCTAppDelegate / ReactAppDependencyProvider framework dependency from
RNXTurboModuleAdapter.

Motivation

React-RCTAppDelegate's RCTAppSetupDefaultModuleFromClass and RCTAppSetupDefaultJsExecutorFactory helpers assume a single-host-per-app shape — process-wide module providers and
executor factories. Multi-host integrations (where each host has its own modules and lifecycle) want neither. Today the only way to use RNXTurboModuleAdapter is to link against
React-RCTAppDelegate even if it's actively the wrong design for the consumer.

Design

When RNX_USE_RCT_APP_DELEGATE_HELPERS=0:

  • getModuleInstanceFromClass: skips RCTAppSetupDefaultModuleFromClass and returns nil. Consumers vending custom-initialized modules do so via
    RNXHostConfig.turboModuleManagerDelegate.getModuleInstanceFromClass: (added in the previous PR). Falling through to nil makes RCTTurboModuleManager use its default [cls new] for
    modules the consumer doesn't override — same behavior the manager has for unknown classes.

  • jsExecutorFactoryForBridge: returns nullptr. Bridgeless integrations (RCTHost-driven) construct the JS runtime via RCTHostJSEngineProvider and never reach this path.
    Bridge-mode consumers wanting this adapter must either keep HELPERS=1 or supply their own RCTBridgeDelegate.jsExecutorFactoryForBridge:.

The macro auto-defaults to 1 when the React-RCTAppDelegate headers are reachable (via __has_include), so behavior is unchanged for existing consumers. Multi-host consumers #define RNX_USE_RCT_APP_DELEGATE_HELPERS 0 in their build settings to opt out.

Why this is useful

Without the previous PR in this stack (the auxiliary turboModuleManagerDelegate hook), opting out of the helpers leaves the consumer with nil-returning defaults and no escape hatch —
pointless. With the auxiliary delegate in place, consumers have a real alternative: implement getModuleInstanceFromClass: themselves to vend whatever modules they need, and
RNX_USE_RCT_APP_DELEGATE_HELPERS=0 drops the framework dependency cleanly.

Test plan

Tested internally

Saadnajmi added 3 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).
…endency

React-RCTAppDelegate's RCTAppSetupDefaultModuleFromClass /
RCTAppSetupDefaultJsExecutorFactory helpers assume a single-host-per-app
shape. Multi-host integrations want their own per-host module providers
and executor factories, not a process-wide default.

Add an `RNX_USE_RCT_APP_DELEGATE_HELPERS` opt-out macro that defaults to
1 (preserving existing behavior). When set to 0:

- `getModuleInstanceFromClass:` returns nil, falling back to
  `RCTTurboModuleManager`'s default `[cls new]` allocation. Consumers
  vending custom-initialized modules do so via
  `RNXHostConfig.turboModuleManagerDelegate`'s
  `getModuleInstanceFromClass:`, added in the previous PR.

- `jsExecutorFactoryForBridge:` returns nullptr. Bridgeless integrations
  (RCTHost-driven) don't need this; bridge-mode consumers wanting this
  adapter must either keep the default (HELPERS=1) or supply their own
  RCTBridgeDelegate that overrides `jsExecutorFactoryForBridge:`.

Depends on the auxiliary `turboModuleManagerDelegate` PR for the
custom-module path to be useful.
@Saadnajmi Saadnajmi marked this pull request as draft May 11, 2026 00:47
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