Organizing screens with registerComponent
ink-cartridge organizes your app with a screen tree: every screen has exactly one parent but can have many children. You switch between screens with methods such as skip and gotoScreen (this chapter only covers how to register screens into the tree — navigation is covered in the "Screen Navigation" chapter).
This chapter covers one of ink-cartridge's most core methods: registerComponent.
The screen tree: parent and children
In ink-cartridge, every React component can be a screen. The relationships between screens are declared by you — the parent option of registerComponent decides "who is this screen's parent".
For example, the following tree:
Main Menu
├── Settings
└── AboutMain Menuhas no parent — it is a root screen;SettingsandAboutboth haveMain Menuas their parent;- navigation only moves between parents and children, or along the tree (
skipdown,backup,gotoScreenacross branches).
Prerequisites
After screens are registered into the tree, they still need to be actually rendered, which is done by two components working together:
ScenarioManagementProvider— the context provider of the screen system, wrapping the whole app: it maintains the screen tree state (current path, layers, modal layers, etc.).defaultScreenspecifies the initial screen at startup (must be a registered component, otherwise it throws), andfullScreenmakes the screen fill the terminal height.CurrentScreen— reads the provider's state and renders the currently active screen. The provider itself renders nothing — the two must be used together.
<ScenarioManagementProvider defaultScreen={Home} fullScreen>
<CurrentScreen />
</ScenarioManagementProvider>If you need the keyboard system,
KeyboardProvidermust be nested insideScenarioManagementProvider, otherwise keyboard bindings will not work.
This is only a brief introduction — their full details will be covered in a later article.
Registering your first screen
Registering a screen takes one line of code:
import React from 'react';
import { Box, Text, render } from 'ink';
import {
CurrentScreen,
ScenarioManagementProvider,
registerComponent,
} from 'ink-cartridge';
function Home() {
return (
<Box flexDirection="column">
<Text bold>🏠 Home</Text>
</Box>
);
}
// Register Home as a screen: no parent declared, so it is a root screen
registerComponent(Home, {});registerComponent is a module-level function — you can call it anywhere in a .ts or .tsx file (usually right after the component definition).
After registering, point ScenarioManagementProvider at the default screen and render:
render(
<ScenarioManagementProvider defaultScreen={Home} fullScreen>
<CurrentScreen />
</ScenarioManagementProvider>
);The terminal will show 🏠 Home. Here:
component(first argument): which component is the screen. The component itself acts as the unique registration key — the same component can only be registered once.template(second argument): the initial template, used as the screen's default props. Props passed during navigation are merged with it.Homeneeds no props, so pass{}.
Building a branching tree
Let's register a "Main Menu → Settings / About" tree to demonstrate one parent, many children:
import React from 'react';
import { Box, Text, render } from 'ink';
import {
CurrentScreen,
ScenarioManagementProvider,
registerComponent,
} from 'ink-cartridge';
function MainMenu() {
return (
<Box flexDirection="column">
<Text bold>🏠 Main Menu</Text>
<Text>Settings and About are my child screens</Text>
</Box>
);
}
registerComponent(MainMenu, {});
function Settings() {
return <Text>⚙️ Settings</Text>;
}
// parent points to MainMenu: Settings becomes a child screen of MainMenu
registerComponent(Settings, {}, { parent: MainMenu });
function About() {
return <Text>ℹ️ About</Text>;
}
// About hangs under MainMenu as well
registerComponent(About, {}, { parent: MainMenu });
render(
<ScenarioManagementProvider defaultScreen={MainMenu} fullScreen>
<CurrentScreen />
</ScenarioManagementProvider>
);Once registered, the screen tree looks like this:
MainMenu (root screen)
├── Settings
└── AboutNote the registration order: register the parent screen first, then the children. If parent is not registered yet, registerComponent throws right away (see caveat #2 below).
API reference
The function signature of registerComponent:
function registerComponent<C extends React.ComponentType<any>>(
component: C,
template: React.ComponentProps<C>,
options?: RegisterOptions,
): void| Argument | Type | Required | Description |
|---|---|---|---|
component | React.ComponentType<any> | Yes | The React component registered as a screen; also serves as the unique registration key |
template | React.ComponentProps<C> | Yes | The initial template, i.e. the screen's default props; merged with props passed during navigation |
options.parent | React.ComponentType<any> | No | The parent screen component; when omitted, the screen is a root screen (candidate) |
The full definition of RegisterOptions:
interface RegisterOptions {
parent?: ComponentType<any> | undefined;
}Caveats
- A component can only be registered once. Calling
registerComponentagain throws:[Ink-Cartridge] Component "xxx" is already registered. Duplicate registration is not allowed.(Avoid duplicate registration in hot-reload or loop scenarios.) parentmust be registered first. When you declare aparent, that component must already be registered, otherwise it throws and tells you to register the parent first:Register the parent first with registerComponent(...).- No
parentmeans a root screen. A root screen has no parent node and is usually passed toScenarioManagementProviderasdefaultScreen, serving as the app's default page and home page. - There can be multiple root screens. Every component without a
parentis the root of its own independent tree; they don't affect each other and each has its own subtree. templateis default props, not "current values". It only describes the default attributes used when the screen is created; props passed during navigation are merged with it, andtemplateitself is never modified.
Next: give the tree an "entry point"
Organizing screens is not enough — without an entry point the screens are dead. registerComponent only registers pages into the tree; to actually "switch pages" you need an entry point. So you bind keys to the current screen with boundKeyboard and call navigation methods inside the callbacks.
boundKeyboard— a foundational method of the keyboard system (fromuseKeyboard()), which binds key events to the current screen;skip— navigate down from the current screen to a child screen;back— return to the parent screen (supportslevelsto go back multiple levels);gotoScreen— jump across branches to any registered screen.
Combined, this is exactly how the minimal app in quick-start is written: press Enter → skip(Detail, {}) to enter a child screen, press Esc → back() to return home.
You can learn along the following path:
- Organizing screens (this chapter) — build the screen tree with
registerComponent; boundKeyboard— learn how to bind keys to a screen (the "Keyboard System" section);skip/back/gotoScreen— perform page switching inside key callbacks (the "Screen Navigation" section).
The concrete usage of these methods is covered in detail in the "Keyboard System" and "Screen Navigation" sections respectively.