React Native Runtimes
Threaded runtime

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 OnRuntime child 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, use threadedComponent instead.
  • 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.
  • OnRuntime accepts 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.

On this page

React Native Runtimes is built with by Margelo

We build fast and beautiful apps. Contact us at margelo.com for high-end consultancy services.

Let's talk