Rendering components on a background 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 native ThreadedRuntimeSurface. 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 defined at module top level.
- Keep the component in module/global scope so Metro can generate the registration and the other runtime can require it by name.
- Props must be JSON-serializable. Pass ids, keys, or small snapshots; 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 you want to provide a custom component
name or avoid the directive transform:
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.