Quick start
Go from zero to rendering on a second JS runtime in about five minutes.
This guide takes a React Native app and lands a top-level component on a second JavaScript runtime. If you already have an app set up, skip to step 2.
Before you start
Make sure you have a working native React Native environment for iOS and Android set up.
Create or open a React Native app
npx @react-native-community/cli@latest init MyApp
cd MyAppInstall the packages
npm install @react-native-runtimes/core @react-native-runtimes/state react-native-nitro-modulesreact-native-nitro-modules is required — both packages are Nitro-backed.
Wrap your Metro config
The Metro plugin generates the entry that secondary runtimes load.
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const { withThreadedRuntime } = require('@react-native-runtimes/core/metro');
const config = {};
module.exports = withThreadedRuntime(
mergeConfig(getDefaultConfig(__dirname), config),
{
roots: ['App.tsx', 'src'],
generatedDir: '.threaded-runtime',
generatedEntry: 'entry.js',
},
);Then add the generated folder to .gitignore:
.threaded-runtime/Load the generated entry inside threaded runtimes
Add this to your index.js so the threaded entry is only required when the
file is loaded inside a secondary runtime:
if (global.__THREADED_RUNTIME_ENV__ || global._is_it_a_list_env === true) {
require('./.threaded-runtime/entry');
}Wire up the native side
import NativeComposeThreadedRuntime
ThreadedRuntime.configure(
withReactNativeDelegate: delegate,
launchOptions: launchOptions
)
// Optional: warm a runtime at startup
ThreadedRuntime.prewarmRuntime("messages-runtime")cd ios
bundle exec pod installimport com.nativecompose.threadedruntime.ThreadedRuntime
import com.nativecompose.threadedzustand.ThreadedZustandPackage
import com.margelo.nitro.NitroModulesPackage
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
ThreadedRuntime.setExtraReactPackagesProvider {
listOf(
NitroModulesPackage(),
ThreadedZustandPackage(),
)
}
loadReactNative(this)
// Optional: warm a runtime at startup
ThreadedRuntime.prewarmRuntime(applicationContext, "messages-runtime")
}
}Render a component on the second runtime
import { OnRuntime } from '@react-native-runtimes/core';
function MessageList({ conversationId }: { conversationId: string }) {
// This entire component runs on 'messages-runtime'.
return <ActualMessageList conversationId={conversationId} />;
}
export default function App() {
return (
<OnRuntime name="messages-runtime">
<MessageList conversationId="release-room" />
</OnRuntime>
);
}That's it. Rebuild the app — your MessageList now runs on its own JS
runtime. The main runtime stays free for navigation, gestures, and the rest of
your UI.
What just happened?
- Metro saw
OnRuntime's direct childMessageList, gave it a stable id, and registered it in the generated threaded entry. - The native
OnRuntimeview asked the runtime namedmessages-runtimeto mount the registered component. - The runtime loaded the same JS bundle as your main runtime, executed the threaded entry, and rendered
MessageList.
Next step
Now read the installation reference for prewarming, headless tasks, and the Metro options — or jump straight to Rendering components on a background runtime.
Troubleshooting
Nothing renders on the secondary runtime
Make sure your index.js loads .threaded-runtime/entry when
global.__THREADED_RUNTIME_ENV__ is set, and that your component is defined
at module scope (not inside another component).
Build error mentioning Nitro
Verify react-native-nitro-modules is installed and pod-install ran without
errors. On Android, confirm NitroModulesPackage() is in the
setExtraReactPackagesProvider list.