From 1685ee42e218207cef8c6777eae2542c8f2b22d6 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 2 May 2026 16:02:04 -0700 Subject: [PATCH] feat(react-native-host): extend RNXHostConfig with optional host hooks 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. --- .../react-native-host/cocoa/RNXHostConfig.h | 20 ++++ .../cocoa/ReactNativeHost.mm | 98 ++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/packages/react-native-host/cocoa/RNXHostConfig.h b/packages/react-native-host/cocoa/RNXHostConfig.h index c14aa71d99..b13e3d9534 100644 --- a/packages/react-native-host/cocoa/RNXHostConfig.h +++ b/packages/react-native-host/cocoa/RNXHostConfig.h @@ -3,6 +3,12 @@ #import #import +#ifdef __cplusplus +#include +#endif + +@class ReactNativeHost; + NS_ASSUME_NONNULL_BEGIN /// Configuration object for ``ReactNativeHost``. @@ -30,6 +36,20 @@ NS_ASSUME_NONNULL_BEGIN /// Handles a fatal error. - (void)onFatalError:(NSError *)error; +/// Called after the JS instance has finished loading. ``error`` is ``nil`` +/// on success. +- (void)host:(ReactNativeHost *)host didLoadInstanceWithError:(nullable NSError *)error + __attribute__((__swift_name__("host(_:didLoadInstanceWithError:)"))); + +/// Called when the instance is about to be unloaded. +- (void)hostWillUnloadInstance:(ReactNativeHost *)host; + +#ifdef __cplusplus +/// Called after host bindings install but before the user JS bundle loads. +/// Use to evaluate pre-user JS on the runtime. Bridgeless mode only. +- (void)host:(ReactNativeHost *)host didInitializeRuntime:(facebook::jsi::Runtime &)runtime; +#endif // __cplusplus + // MARK: - RCTBridgeDelegate deprecated details (for backwards compatibility) [>=0.84] - (NSURL *__nullable)sourceURLForBridge:(RCTBridge *)bridge; diff --git a/packages/react-native-host/cocoa/ReactNativeHost.mm b/packages/react-native-host/cocoa/ReactNativeHost.mm index 677b9e8a38..ee65a924e3 100644 --- a/packages/react-native-host/cocoa/ReactNativeHost.mm +++ b/packages/react-native-host/cocoa/ReactNativeHost.mm @@ -35,6 +35,41 @@ @interface ReactNativeHost () @end #endif // USE_CODEGEN_PROVIDER +#if USE_BRIDGELESS + +// Forwards host:didInitializeRuntime: from RCTHost to the consumer's RNXHostConfig. +@interface _RNXForwardingRCTHostDelegate : NSObject +- (instancetype)initWithHost:(ReactNativeHost *)host config:(id)config; +@end + +@implementation _RNXForwardingRCTHostDelegate { + __weak ReactNativeHost *_host; + __weak id _config; +} + +- (instancetype)initWithHost:(ReactNativeHost *)host config:(id)config +{ + if (self = [super init]) { + _host = host; + _config = config; + } + return self; +} + +- (void)host:(RCTHost *)host didInitializeRuntime:(facebook::jsi::Runtime &)runtime +{ + id config = _config; + ReactNativeHost *forwardedHost = _host; + if (forwardedHost != nil && + [config respondsToSelector:@selector(host:didInitializeRuntime:)]) { + [config host:forwardedHost didInitializeRuntime:runtime]; + } +} + +@end + +#endif // USE_BRIDGELESS + @implementation ReactNativeHost { __weak id _config; NSDictionary *_launchOptions; @@ -44,6 +79,9 @@ @implementation ReactNativeHost { RCTHost *_reactHost; NSLock *_isShuttingDown; RNXHostReleaser *_hostReleaser; +#if USE_BRIDGELESS + _RNXForwardingRCTHostDelegate *_hostDelegateProxy; +#endif // USE_BRIDGELESS #ifdef USE_REACT_NATIVE_CONFIG std::shared_ptr _reactNativeConfig; #endif // USE_REACT_NATIVE_CONFIG @@ -99,11 +137,63 @@ - (instancetype)initWithConfig:(id)config launchOptions:(NSDictio }); } + if ([config respondsToSelector:@selector(host:didLoadInstanceWithError:)]) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(_rnxInstanceDidLoad:) + name:RCTJavaScriptDidLoadNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(_rnxInstanceDidFailToLoad:) + name:RCTJavaScriptDidFailToLoadNotification + object:nil]; + } + if ([config respondsToSelector:@selector(hostWillUnloadInstance:)]) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(_rnxInstanceWillUnload:) + name:RCTBridgeWillBeInvalidatedNotification + object:nil]; + } + [self initializeReactHost]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)_rnxInstanceDidLoad:(NSNotification *)__unused notification +{ + id config = _config; + if ([config respondsToSelector:@selector(host:didLoadInstanceWithError:)]) { + [config host:self didLoadInstanceWithError:nil]; + } +} + +- (void)_rnxInstanceDidFailToLoad:(NSNotification *)notification +{ + id config = _config; + if ([config respondsToSelector:@selector(host:didLoadInstanceWithError:)]) { + NSError *error = notification.userInfo[@"error"]; + [config host:self didLoadInstanceWithError:error ?: [NSError errorWithDomain:@"ReactNativeHost" + code:0 + userInfo:nil]]; + } +} + +- (void)_rnxInstanceWillUnload:(NSNotification *)__unused notification +{ + id config = _config; + if ([config respondsToSelector:@selector(hostWillUnloadInstance:)]) { + [config hostWillUnloadInstance:self]; + } +} + - (RCTBridge *)bridge { if (self.isBridgelessEnabled) { @@ -303,6 +393,10 @@ - (void)initializeReactHost #endif }; + // Retained as an ivar because RCTHost stores host delegates weakly. + _hostDelegateProxy = [[_RNXForwardingRCTHostDelegate alloc] initWithHost:self + config:_config]; + __weak __typeof(self) weakSelf = self; if ([RCTHost instancesRespondToSelector:@selector (initWithBundleURLProvider: @@ -312,13 +406,13 @@ - (void)initializeReactHost initWithBundleURLProvider:^{ return [weakSelf sourceURLForBridge:nil]; } - hostDelegate:nil + hostDelegate:_hostDelegateProxy turboModuleManagerDelegate:_turboModuleAdapter jsEngineProvider:jsEngineProvider launchOptions:_launchOptions]; } else { _reactHost = [[RCTHost alloc] initWithBundleURL:[self sourceURLForBridge:nil] - hostDelegate:nil + hostDelegate:_hostDelegateProxy turboModuleManagerDelegate:_turboModuleAdapter jsEngineProvider:jsEngineProvider]; }