Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lucky-pears-preload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@callstack/react-native-brownfield': minor
---

Add `preloadReactNative` on iOS. `startReactNative` only makes the factory, and React Native loads the JavaScript bundle when it creates the first React Native view. `preloadReactNative` moves that work to the app launch. On Android, `ReactNativeBrownfield.initialize` does the same operation.

`onBundleLoaded` now runs on the main thread, and not on the JavaScript thread. `startReactNative` and `preloadReactNative` can each have a callback, because a new callback joins the callbacks that wait and does not replace them. A callback that you add after React Native loaded the bundle runs immediately.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Starts React Native, produces an instance of React Native. You can use it to ini

| Param | Required | Type | Description |
| ---------------- | -------- | --------------- | ------------------------------------------------- |
| `onBundleLoaded` | No | `void(^)(void)` | Callback invoked after JS bundle is fully loaded. |
| `onBundleLoaded` | No | `void(^)(void)` | Callback invoked on the main thread after JS bundle is fully loaded. |

Brownfield calls `onBundleLoaded` one time, on the main thread. If React Native already loaded the bundle, the callback runs immediately. The callback joins the callbacks that already wait, and does not replace them.

**Examples:**

Expand All @@ -60,6 +62,37 @@ Starts React Native, produces an instance of React Native. You can use it to ini
}];
```

##### `preloadReactNative`

Starts React Native and loads the JavaScript bundle immediately. If you do not call this method, React Native loads the bundle when it creates the first React Native view. On Android, `ReactNativeBrownfield.initialize` does the same operation. See [Preload the JavaScript bundle](/docs/getting-started/ios#preload-the-javascript-bundle).

You can call this method more than one time. You can also call it together with `startReactNative`.

| Param | Required | Type | Description |
| ---------------- | -------- | --------------- | ------------------------------------------------------------------------------- |
| `launchOptions` | No | `NSDictionary` | The launch options for the React Host. Usually you get them from `AppDelegate`. |
| `onBundleLoaded` | No | `void(^)(void)` | Callback invoked on the main thread after JS bundle is fully loaded. |

`onBundleLoaded` follows the rules of [`startReactNative`](#startreactnative). Thus `startReactNative` and `preloadReactNative` can each have a callback.

Only the call that creates the React Host reads the launch options. Brownfield keeps the options of this method, also if it cannot create the host now. A view that creates the host later uses them. Options that you give to `view` win over these options.

**Examples:**

```objc
[[ReactNativeBrownfield shared] preloadReactNative];
```

```objc
[[ReactNativeBrownfield shared] preloadReactNativeWithLaunchOptions:launchOptions
onBundleLoaded:^(void){
NSLog(@"React Native bundle loaded");
}];
```

> [!Note]
> With Expo, this method can only prepare the runtime, because Expo can select the bundle after the app starts. See [Expo Integration](/docs/getting-started/expo#xcframework-present-rn-ui).

##### `stopReactNative`

Stops React Native and releases the underlying runtime. Safe to call multiple times. Call it after all React Native views are dismissed.
Expand Down
34 changes: 33 additions & 1 deletion docs/docs/docs/api-reference/react-native-brownfield/swift.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Starts React Native. You can use it to initialize React Native in your app.

| Param | Required | Type | Description |
| ---------------- | -------- | --------------- | ------------------------------------------------- |
| `onBundleLoaded` | No | `(() -> Void)?` | Callback invoked after JS bundle is fully loaded. |
| `onBundleLoaded` | No | `(() -> Void)?` | Callback invoked on the main thread after JS bundle is fully loaded. |

Brownfield calls `onBundleLoaded` one time, on the main thread. If React Native already loaded the bundle, the callback runs immediately. The callback joins the callbacks that already wait, and does not replace them.

**Examples:**

Expand All @@ -60,6 +62,36 @@ ReactNativeBrownfield.shared.startReactNative(onBundleLoaded: {
})
```

##### `preloadReactNative`

Starts React Native and loads the JavaScript bundle immediately. If you do not call this method, React Native loads the bundle when it creates the first React Native view. On Android, `ReactNativeBrownfield.initialize` does the same operation. See [Preload the JavaScript bundle](/docs/getting-started/ios#preload-the-javascript-bundle).

You can call this method more than one time. You can also call it together with `startReactNative`.

| Param | Required | Type | Description |
| ---------------- | -------- | --------------------- | ------------------------------------------------------------------------------- |
| `launchOptions` | No | `[AnyHashable: Any]?` | The launch options for the React Host. Usually you get them from `AppDelegate`. |
| `onBundleLoaded` | No | `(() -> Void)?` | Callback invoked on the main thread after JS bundle is fully loaded. |

`onBundleLoaded` follows the rules of [`startReactNative`](#startreactnative). Thus `startReactNative` and `preloadReactNative` can each have a callback.

Only the call that creates the React Host reads the launch options. Brownfield keeps the options of this method, also if it cannot create the host now. A view that creates the host later uses them. Options that you give to `view` win over these options.

**Examples:**

```swift
ReactNativeBrownfield.shared.preloadReactNative()
```

```swift
ReactNativeBrownfield.shared.preloadReactNative(launchOptions: nil) {
print("React Native bundle loaded")
}
```

> [!Note]
> With Expo, this method can only prepare the runtime, because Expo can select the bundle after the app starts. See [Expo Integration](/docs/getting-started/expo#xcframework-present-rn-ui).

##### `stopReactNative`

Stops React Native and releases the underlying runtime. Safe to call multiple times. Call it after all React Native views are dismissed.
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/docs/getting-started/expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ struct IosApp: App {
}
```

To load the JavaScript bundle at the app launch, use [`preloadReactNative`](/docs/api-reference/react-native-brownfield/swift#preloadreactnative).

> [!Note]
> The React Host keeps the first bundle that it evaluates, thus `preloadReactNative` waits until the bundle URL is stable. While Expo can still select a different bundle, `preloadReactNative` only prepares the runtime, and the first React Native screen loads the bundle. This occurs in a Debug build with `expo-dev-client`, until the user selects an app in the launcher. This also occurs with `expo-updates` in a Release build, until Expo selects the update. Brownfield keeps the launch options of the call, and the first React Native screen uses them. If you set `bundleURLOverride`, the bundle URL cannot change, and `preloadReactNative` always loads the bundle.

If you package the framework in **Debug** and want to run it without Metro, enable the embedded bundle explicitly before calling `startReactNative`:

```swift
Expand Down
21 changes: 20 additions & 1 deletion docs/docs/docs/getting-started/ios.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
ReactNativeBrownfield.shared.startReactNative(onBundleLoaded: {
print("React Native bundle loaded")
}, launchOptions: launchOptions)
})

window = UIWindow(frame: UIScreen.main.bounds)

Expand All @@ -232,6 +232,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
```

`startReactNative` does not take the launch options. Give them to `preloadReactNative`, which is in the next section.

### Preload the JavaScript bundle

`startReactNative` only prepares the runtime. React Native creates and starts the host in `viewWithModuleName:`. Thus React Native loads and evaluates the bundle only when it creates the first React Native screen. React Native also calls `onBundleLoaded` at that time.

To do this work at the app launch, call `preloadReactNative`. On Android, `ReactNativeBrownfield.initialize` does the same operation:

```swift
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
ReactNativeBrownfield.shared.preloadReactNative(launchOptions: launchOptions) {
print("React Native bundle loaded")
}
```

`preloadReactNative` adds overhead to the caller's section, but speeds up the first React Native screen appearance. If you preload React Native in your app's launch critical section, then it impacts your app's launch time.

For the rules of `onBundleLoaded` and of the launch options, see [`preloadReactNative`](/docs/api-reference/react-native-brownfield/swift#preloadreactnative).

## 9. Run Your App

### Debug Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/**
* Creates and starts the React Host, but does not create a view. React Native loads and evaluates
* the JavaScript bundle immediately, and not when it mounts the first React Native view.
* `viewWithModuleName:` makes the same call internally, thus a view that you create later uses the
* host that is ready.
*
* This class is in Objective-C, because Swift cannot make this call:
* `RCTReactNativeFactory.devMenuConfiguration` is nullable, but the related parameter is not
* nullable.
*/
@interface BrownfieldReactHostPreloader : NSObject

/**
* Call this method on the main thread. If a host is already available, this method does nothing.
*
* @param reactNativeFactory An `RCTReactNativeFactory`, or a subclass, for example
* `ExpoReactNativeFactory`. The type is `id`, because this header must stay free of the React
* Native imports.
* @param launchOptions The launch options for the React Host.
*/
+ (void)preloadWithReactNativeFactory:(id)reactNativeFactory
launchOptions:(nullable NSDictionary *)launchOptions;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#import "BrownfieldReactHostPreloader.h"

// This file is plain Objective-C, and not Objective-C++. The umbrella header of
// React-RCTAppDelegate imports the Hermes and JSI headers if `__cplusplus` is set. This pod does
// not have the necessary search paths for those headers. Without C++, the compiler ignores those
// imports. The compiler then builds the module in the same way that Swift imports it.
//
// The name of the pod changes if the build makes it a framework. Thus you must try each possible
// name. Expo uses the same method in `RCTAppDelegateUmbrella.h`.
#if __has_include(<React_RCTAppDelegate/RCTRootViewFactory.h>)
#import <React_RCTAppDelegate/RCTReactNativeFactory.h>
#import <React_RCTAppDelegate/RCTRootViewFactory.h>
#elif __has_include(<React-RCTAppDelegate/RCTRootViewFactory.h>)
#import <React-RCTAppDelegate/RCTReactNativeFactory.h>
#import <React-RCTAppDelegate/RCTRootViewFactory.h>
#else
#import <React/RCTReactNativeFactory.h>
#import <React/RCTRootViewFactory.h>
#endif

NS_ASSUME_NONNULL_BEGIN

// `RCTBundleConfiguration` first exists in React Native 0.84. React Native 0.83 does not declare
// the class. This forward declaration lets the file name the type with both versions.
@class RCTBundleConfiguration;

/**
* The two shapes of `initializeReactHostWithLaunchOptions:...`. React Native 0.83 has the shape
* without `bundleConfiguration:`. React Native 0.84 added that parameter and removed the earlier
* shape. No header tells the two versions apart at compile time, thus this file declares both
* shapes and `respondsToSelector:` selects one at run time. The declarations are equal to the
* React Native declarations. The compiler then accepts the file with both versions.
*/
@protocol BrownfieldReactHostInitializing <NSObject>

- (void)initializeReactHostWithLaunchOptions:(NSDictionary *__nullable)launchOptions
bundleConfiguration:(RCTBundleConfiguration *)bundleConfiguration
devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration;

- (void)initializeReactHostWithLaunchOptions:(NSDictionary *__nullable)launchOptions
devMenuConfiguration:(RCTDevMenuConfiguration *)devMenuConfiguration;

@end

/**
* `RCTReactNativeFactory.bundleConfiguration` also first exists in React Native 0.84.
*/
@protocol BrownfieldBundleConfigurationProviding <NSObject>

@property (nonatomic, readonly) RCTBundleConfiguration *bundleConfiguration;

@end

NS_ASSUME_NONNULL_END

@implementation BrownfieldReactHostPreloader

+ (void)preloadWithReactNativeFactory:(id)reactNativeFactory
launchOptions:(NSDictionary *)launchOptions
{
Comment on lines +58 to +60
RCTReactNativeFactory *factory = (RCTReactNativeFactory *)reactNativeFactory;
RCTRootViewFactory *rootViewFactory = factory.rootViewFactory;

// `initializeReactHostWithLaunchOptions:...` returns early if a view, or an earlier preload,
// created a host. Thus this method only checks the factory.
if (rootViewFactory == nil) {
return;
}

id<BrownfieldReactHostInitializing> hostInitializer = (id)rootViewFactory;
SEL initializeWithBundleConfiguration =
@selector(initializeReactHostWithLaunchOptions:bundleConfiguration:devMenuConfiguration:);

// Get the configurations from the factory. Do not make new configurations from the default
// values. The preloaded host is then the same as a host that a view creates later.
// `RCTReactNativeFactory.startReactNativeWithModuleName:...` sends the same configurations. The
// default value of `devMenuConfiguration` is nil. React Native then uses its own default
// configuration.
if ([hostInitializer respondsToSelector:initializeWithBundleConfiguration]) {
id<BrownfieldBundleConfigurationProviding> configurationProvider = (id)factory;

[hostInitializer initializeReactHostWithLaunchOptions:launchOptions
bundleConfiguration:configurationProvider.bundleConfiguration
devMenuConfiguration:factory.devMenuConfiguration];
} else {
[hostInitializer initializeReactHostWithLaunchOptions:launchOptions
devMenuConfiguration:factory.devMenuConfiguration];
}
}

@end
Loading