Component model
Ratatui Kit’s component model has two layers: Element is the declaration recreated every frame, while Component is the persistent instance reused by the runtime. In application code, you mostly declare UI inside element!; during update, the framework reconciles those declarations into a component tree and preserves hooks, subtrees, layout, and lifecycle state.
If you come from React, this is similar to the relationship between JSX elements and component instances. The difference is that the output is a Ratatui buffer, not a DOM tree.
What happens in one frame
Section titled “What happens in one frame”A function component runs again every frame:
The element!(Text(...)) call is a declaration. The thing that actually stores count is not this temporary declaration tree, but the instantiated Counter node in the runtime and its hook list.
Element is a declaration
Section titled “Element is a declaration”You can think of Element<T> as:
It is lightweight and can be rebuilt every frame. The element! macro turns declarative syntax into an element tree:
AnyElement is the type-erased version that lets different component types live in the same children list. See Declarative syntax for the full macro syntax.
Component is an instance
Section titled “Component is an instance”Component is the persistent object. An instance owns:
- The component’s own state.
- Its hook list.
- Its child component tree.
- Its layout style.
- Lifecycle callbacks.
Function components get a generated Component implementation from #[component]. You write a function; the runtime sees a transparent wrapper component that runs the function body during each update, obtains the returned root element, and reconciles its subtree.
Hand-written components implement Component directly. This is usually for native Ratatui widget bridges or custom layout:
See Native widget bridge for the complete example.
Reuse rules
Section titled “Reuse rules”When reconciling children, the runtime matches current declarations to previous-frame nodes using ElementKey + TypeId:
| Current declaration | Previous instance | Result |
|---|---|---|
| Same key, same component type | Match | Reuse instance, preserve hooks and subtree |
| Same key, different component type | Type changed | Recreate instance |
| Key changed | Identity changed | Recreate instance |
| Old key disappeared | No declaration | Unmount instance and run on_drop |
This is why list rendering needs stable keys:
Do not use array indices as keys when the list can insert, delete, or reorder items. After inserting one row, every following index changes, and old state may be reused on the wrong row.
Static small arrays can use indices. For example, ROWS in Control flow syntax is a fixed array, so key: index is stable.
Control flow belongs to the declaration layer
Section titled “Control flow belongs to the declaration layer”element! supports if, if let, for, and match directly:
This control flow only changes the element declarations produced for the current frame. The runtime still uses key and type to decide which instances can be reused.
That explains an important boundary: you may use conditional rendering inside element!, but do not put hook calls inside conditional branches. Hook order belongs to the component instance and must be stable every frame; render branches belong to the declaration layer and may change every frame. See Hooks for more rules.
Props are the data passed to a component declaration. element! constructs the props struct from fields and fills missing fields with Default::default():
Custom props use #[derive(Props)]:
If a props field cannot derive Default, write a semantically neutral impl Default. This is not cosmetic: element! depends on default values to fill omitted fields.
Optional fields should use Option<T>. The macro calls .into() on field values, so top_title: Line::from("title"), top_title: Some(line), and top_title: None all work.
Layout props
Section titled “Layout props”Ratatui Kit layout fields come from LayoutStyle:
widthheightflex_directionjustify_contentgapmarginoffset
Built-in components expose these fields on props. Custom components that want to support layout props should add #[with_layout_style] to their props.
Function components have one special rule: the component generated by #[component] is transparent for layout. It does not occupy a separate layout node; it inherits the layout style of the first child element it returns.
So layout props should be written on the root element returned by the function component:
Do not expect Panel(width: ...) at the parent site to take effect unless Panel is a props-bearing hand-written component or explicitly forwards layout fields to its internal root.
Custom layout
Section titled “Custom layout”The default component layout is flex-like child area splitting. Components can override calc_children_areas for non-flex behavior:
ScrollViewcombines content area with scroll offset.Modalpositions the surface in the screen center and injects an input layer into the subtree.- Custom canvases, tables, and split workspaces may need their own area calculation.
calc_children_areas must return exactly the same number of areas as there are children. Too few areas means a child will not draw; extra areas have no matching child. Debug builds try to expose these contract violations.
Where to read next
Section titled “Where to read next”- For
if / for / matchinside trees, read Control flow syntax. - For
element!,#[component], props, and adapter rules, read Declarative syntax. - For why state persists across frames, read Hooks and State.
- For native Ratatui widget integration, read Native widget bridge.
- For the whole update / draw loop, read Render loop.