Rendering components on a secondary runtime
Use OnRuntime, ThreadedScreen, and threadedComponent to mount React components on a named secondary runtime.
Mount a top-level component inside OnRuntime. Metro treats the direct child
as a threaded boundary.
import { OnRuntime } from '@react-native-runtimes/core';
type MessageListProps = {
conversationId: string;
initialIndex?: number;
};
function MessageList(props: MessageListProps) {
return <ActualMessageList {...props} />;
}
export function ConversationPreview() {
return (
<OnRuntime name="messages-runtime">
<MessageList conversationId="release-room" initialIndex={120} />
</OnRuntime>
);
}Metro sees MessageList as the direct child of OnRuntime, rewrites it into
the same registration shape as threadedComponent(...), gives it a stable
file-based id, and exports it so the generated threaded runtime entry can load
it with require(file).MessageList.
OnRuntime renders a ThreadedReactSurface component, which is backed by a
native surface. The surface asks the named runtime to render
ThreadedRuntimeHost, and that host resolves the registered component by name.
Rules
- The
OnRuntimechild must be a direct component reference, such as<MessageList />. - The child component must be a function declaration at module top level, in the same file as the
<OnRuntime>usage. For components imported from other files, usethreadedComponentinstead. - Props must be JSON-serializable, and they are mount-time only — changing them remounts the surface. Pass ids and keys; read large or mutable data through a shared store.
OnRuntimeaccepts one threaded component child.- Because Metro rewrites the function into an exported const, define it before code that calls it during module initialization.
Tip: ids over data
Pass the identity of what to render — a conversation id, a workspace key — not the data itself. Then load the data from a shared store inside the threaded component. This keeps the prop payload tiny and the threaded boundary cheap.
Whole screens
For a whole route, use ThreadedScreen. It applies a full-size surface style
and preloads the runtime by default.
import { ThreadedScreen, threadedComponent } from '@react-native-runtimes/core';
type ConversationScreenProps = {
conversationId: string;
};
export const ConversationScreen = threadedComponent<ConversationScreenProps>(
'ConversationScreen',
function ConversationScreen({ conversationId }) {
return <ConversationRoute conversationId={conversationId} />;
},
);
<ThreadedScreen
component={ConversationScreen}
props={{ conversationId: 'release-room' }}
runtimeName="conversation-release-room-runtime"
testID="conversation-threaded-screen"
/>;Keep runtime names stable. If the name changes, native creates or switches to another runtime.
Explicit registration
Use threadedComponent directly when the component lives in its own file, or
when you want to control the registration name:
import { Threaded, threadedComponent } from '@react-native-runtimes/core';
export const MessageList = threadedComponent<MessageListProps>(
'MessageList',
function MessageList(props) {
return <ActualMessageList {...props} />;
},
);
<Threaded
component={MessageList}
props={{ conversationId: 'release-room', initialIndex: 120 }}
runtimeName="messages-runtime"
/>;When to use it
- Long chat or feed screens
- Heavy list renderers such as FlashList or LegendList
- Components that can keep working while the main JS runtime is blocked
- Screens where native navigation should stay responsive even when the screen's JS work is busy
Don't push trivial UI to a runtime
The runtime itself has a startup cost (small but real) and a per-frame rendering cost. Threaded surfaces pay off when you avoid main-runtime blocking, not when the surface is a button.