Skip to content
This repository has been archived by the owner. It is now read-only.

Commit

Permalink
feat: Automagically load experiences on demand (#14)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `ReactExperienceLoader.load()` has been removed.
Modules registered using `AppRegistry` or `BatchedBridge` are now
automatically loaded on demand. The configuration schema has also
been changed to reflect this.
  • Loading branch information
tido64 authored Dec 9, 2020
1 parent 1c20c1f commit 2444d45
Show file tree
Hide file tree
Showing 32 changed files with 2,974 additions and 510 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install
run: yarn ci
- name: Check
run: yarn tsc
- name: Lint
run: yarn lint
- name: Test
run: yarn test
release:
Expand Down
63 changes: 3 additions & 60 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,61 +1,4 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
coverage/
node_modules/
!test/__fixtures__/*/node_modules/
5 changes: 5 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
*.out
*.tgz
.github/
.vscode/
coverage/
test/
tsconfig.json
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"[javascript]": {
"editor.formatOnSave": true
},
"[json]": {
"editor.formatOnSave": true
}
}
165 changes: 100 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,101 +32,136 @@ it to your `.babelrc`:
```

In your `package.json`, add a section called `"experiences"` with the features
that should be lazy loaded. In the example below, we have four features keyed on
unique names:
that should be lazy loaded. In the example below, we've listed four packages:

```json
{
"name": "MyAwesomeApp",
"name": "my-awesome-app",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@MyAwesomeApp/SomeFeature": "*",
"@MyAwesomeApp/AnotherFeature": "*",
"@MyAwesomeApp/YetAnotherFeature": "*",
"@MyAwesomeApp/FinalFeature": "*",
"react": "16.9.0",
"react-native": "0.61.4",
"react-native-lazy-index": "^1.0.0"
"@awesome-app/some-feature": "*",
"@awesome-app/another-feature": "*",
"@awesome-app/yet-another-feature": "*",
"@awesome-app/final-feature": "*",
"react": "16.13.1",
"react-native": "0.63.4",
"react-native-lazy-index": "^2.0.0"
},
"experiences": {
"Some": "@MyAwesomeApp/SomeFeature",
"Another": "@MyAwesomeApp/AnotherFeature",
"YetAnother": "@MyAwesomeApp/YetAnotherFeature",
"Final": "@MyAwesomeApp/FinalFeature"
}
"experiences": [
"@awesome-app/some-feature",
"@awesome-app/another-feature",
"@awesome-app/yet-another-feature",
"@awesome-app/final-feature"
]
}
```

Import `react-native-lazy-index` in your `index.js`:
That's it!

## Why

With a naive `index.js`, all features will be loaded when your app starts and
React Native is initialized for the first time.

```js
import "react-native-lazy-index";
import "@awesome-app/some-feature";
import "@awesome-app/another-feature";
import "@awesome-app/yet-another-feature";
import "@awesome-app/final-feature";
```

On the native side, you can now load your experiences by invoking
`ReactExperienceLoader.load()`. As an example, we will load two features,
`AnotherFeature` and `YetAnotherFeature`:
By loading features on demand, we can improve app startup time.

```objc
// iOS
[bridge enqueueJSCall:@"ReactExperienceLoader"
method:@"load"
args:@[@"Another", @"YetAnother"]
completion:nil];
```
With `react-native-lazy-index`, we no longer load all features up front.
Instead, `index.js` wraps calls to `AppRegistry.registerComponent` and
`BatchedBridge.registerCallableModule`, deferring the import of a feature until
it is used. Features that are never used, are never loaded.

```java
// Android
ReactInstanceManager reactInstanceManager = reactNativeHost.getReactInstanceManager();
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
CatalystInstance catalystInstance = reactContext.getCatalystInstance();
When you import `react-native-lazy-index`, something similar to below is
generated:

WritableNativeArray features = new WritableNativeArray();
features.pushString("Another");
features.pushString("YetAnother");
```js
const { AppRegistry } = require("react-native");

AppRegistry.registerComponent("SomeFeature", () => {
// We'll import the module the first time "SomeFeature" is accessed.
require("@awesome-app/some-feature");
// "SomeFeature" is now overwritten and we can return the real component.
// Subsequent calls to "SomeFeature" will no longer go through this wrapper.
return AppRegistry.getRunnable("SomeFeature").componentProvider();
});

AppRegistry.registerComponent("AnotherFeature", () => {
// We'll import the module the first time "AnotherFeature" is accessed.
require("@awesome-app/another-feature");
// "AnotherFeature" is now overwritten and we can return the real component.
// Subsequent calls to "AnotherFeature" will no longer go through this
// wrapper.
return AppRegistry.getRunnable("AnotherFeature").componentProvider();
});

catalystInstance.callFunction("ReactExperienceLoader", "load", features);
AppRegistry.registerComponent("YetAnotherFeature", () => {
// We'll import the module the first time "YetAnotherFeature" is accessed.
require("@awesome-app/yet-another-feature");
// "YetAnotherFeature" is now overwritten and we can return the real
// component. Subsequent calls to "YetAnotherFeature" will no longer go
// through this wrapper.
return AppRegistry.getRunnable("YetAnotherFeature").componentProvider();
});

AppRegistry.registerComponent("FinalFeature", () => {
// We'll import the module the first time "FinalFeature" is accessed.
require("@awesome-app/final-feature");
// "FinalFeature" is now overwritten and we can return the real component.
// Subsequent calls to "FinalFeature" will no longer go through this wrapper.
return AppRegistry.getRunnable("FinalFeature").componentProvider();
});
```

## Why
## Troubleshooting

With a naive `index.js`, all features will be loaded when your app starts and
React Native is initialized for the first time.
If you're having trouble with undetected components, there are a couple of
things you should look out for.

### First parameter must be a string literal

`react-native-lazy-index` cannot evaluate the name passed to
`AppRegistry.registerComponent()` or `BatchedBridge.registerCallableModule()`
unless it is a string literal. For instance, if you have something like this in
code:

```js
import "@MyAwesomeApp/SomeFeature";
import "@MyAwesomeApp/AnotherFeature";
import "@MyAwesomeApp/YetAnotherFeature";
import "@MyAwesomeApp/FinalFeature";
```
const appName = "MyApp";

By loading features on demand, we can improve app startup time.
AppRegistry.registerComponent(appName, () => {
...
});
```

With `react-native-lazy-index`, we no longer load all features up front.
Instead, `index.js` registers a callable module, `ReactExperienceLoader`,
allowing full control over when a feature is loaded. Features that are never
used, should never be loaded.
You'll need to inline the string:

```js
const BatchedBridge = require("react-native/Libraries/BatchedBridge/BatchedBridge");
BatchedBridge.registerCallableModule("ReactExperienceLoader", {
load: (...names) =>
names.forEach(name => {
switch (name) {
case "SomeFeature":
return require("@MyAwesomeApp/SomeFeature");
case "AnotherFeature":
return require("@MyAwesomeApp/AnotherFeature");
case "YetAnotherFeature":
return require("@MyAwesomeApp/YetAnotherFeature");
case "FinalFeature":
return require("@MyAwesomeApp/FinalFeature");
}
})
AppRegistry.registerComponent("MyApp", () => {
...
});
```

`react-native-lazy-index` outputs warnings when it detects these instances.

### My components are still not found

`react-native-lazy-index` avoids scanning dependencies too deeply to reduce its
impact on the build time. If your registrations lie too deep within a
dependency, it may have bailed out before reaching them. There are a couple of
things you can do to help `react-native-lazy-index` find your components:

1. If you have access to the source code, you can move your registrations
further up, closer to the entry point of your dependency.
2. You can increase the max depth by setting the environment variable
`RN_LAZY_INDEX_MAX_DEPTH`. The default is currently set to 3. Note that
changing this setting may significantly impact your build time.

## Contributing

This project welcomes contributions and suggestions. Most contributions require
Expand Down
Loading

0 comments on commit 2444d45

Please sign in to comment.