声明式语法
Ratatui Kit 的日常写法由几组宏支撑:
element!:声明本帧 UI 树。#[component]:把函数变成持久Component。#[derive(Props)]:让 props 能进入运行时类型擦除系统。#[with_layout_style]:给组件 props 注入布局字段。routes!:声明 Router 路由表。
这页是语法参考。想先看它在运行时如何保留状态,读 组件模型;想看 if / for / match 的真实例子,读 控制流语法。
element!
Section titled “element!”最常见的形式是:
圆括号里是 props,花括号里是 children。没有 props 时可以省略圆括号;没有 children 时可以省略花括号。
字段值会统一走 .into(),所以很多 props 能接收比字段类型更轻的写法:
element! 会用 Default::default() 补齐未传字段。这意味着用字段语法构造的自定义 props 需要实现 Default。
如果 props 无法提供合理默认值,可以先在普通 Rust 代码里构造完整 props,再用 ..props 整体传入:
..rest 必须放在 props 列表最后。使用 rest props 时,宏不会再追加 ..Default::default()。
控制流是一等语法
Section titled “控制流是一等语法”children 里可以直接写 if、if let、for 和 match:
不同分支可以返回不同组件类型,不需要手写 .into_any()。这是声明层控制流;不要把 hook 调用放进这些分支里。Hook 调用顺序属于组件实例,必须每帧稳定。
复杂动态表达式仍然可以用 { expr } 嵌入:
优先使用一等控制流;只有当表达式已经天然返回 Option、Vec、iterator 或 element 时,再用 { expr }。
key: 是保留字段,不会进入 props。它只影响 element 身份:
运行时按 ElementKey + TypeId 复用上一帧组件实例。列表会插入、删除或重排时,用业务 id;只有静态数组或顺序永远不变的小列表,才适合用 index。
routes! 里不能给路由组件写 key:。路由身份由 path 决定,宏会明确拒绝这种写法。
#[component]
Section titled “#[component]”函数组件用 #[component]:
宏只识别这些参数名:
| 参数名 | 作用 |
|---|---|
hooks / _hooks | 接收 Hooks,可以按值或引用 |
props / _props | 接收 props 引用,类型必须是引用 |
其它参数名会报错。这不是风格限制,而是宏靠参数名决定要把什么传给生成的 implementation。
函数组件是透明布局组件:它不占独立布局节点,而是把返回的根 element 交给运行时。布局属性应该写在返回的根元素上:
如果你希望父级写 Panel(width: ...) 并让它自己占位,就不要把它做成无 props 的透明包装;改成带 props 并把布局字段显式传给内部根元素,或者手写 Component。
自定义 props 需要实现 Props:
不能 derive(Default) 时,手写一个语义中性的默认值:
对于没有合理默认值的字段,例如 State<ListState>,优先在调用点构造完整 props,然后用 Component(..props) 传入。不要为了满足 Default 伪造无效状态句柄。
可选字段直接用 Option<T>。因为字段值会 .into(),下面三种写法都成立:
ratatui 0.30 的 Block<'static> 可以直接作为 props 字段,例如 block: Option<Block<'static>>。不要再引入 Send/Sync 包装;当前运行时是单线程渲染,框架层也不再强制 props 必须 Send + Sync。
无 props 的手写组件用 NoProps:
函数组件通常不需要直接写 NoProps;#[component] 会生成对应的 props 类型。只有手写 Component、adapter 或测试 helper 时,才需要显式选择 NoProps。
运行时内部会把不同组件的 props 装进 AnyProps。这是类型擦除容器,正确性依赖协调阶段已经确认组件类型和 props 类型匹配。应用代码不应该直接构造或 downcast AnyProps;写组件时只暴露具体的 Props 类型,让 element! 和运行时处理擦除。
回调 Props
Section titled “回调 Props”内置组件的 on_change、on_select、on_close 这类回调用 Handler<'a, T, V = ()> 表达。调用方通常不需要手写 Handler::from,因为 element! 会对字段值调用 .into():
自定义组件如果要暴露回调,也优先用同一个类型:
Handler::default() 是空实现,返回值类型 V 需要实现 Default。组件内部如果要把回调移进事件闭包,先用 take() 取出当前 handler;需要区分用户有没有传回调时,用 is_default()。
with_layout_style
Section titled “with_layout_style”组件想支持 width、height、margin、offset、gap、flex_direction 和 justify_content,给 props 加 #[with_layout_style]:
也可以只注入部分布局字段:
#[with_layout_style] 只能用于具名字段结构体。它会生成 layout_style() 方法,手写组件需要在 update 里调用:
widget / stateful adapter
Section titled “widget / stateful adapter”element! 可以直接桥接 ratatui widget:
widget(expr) 只接收一个表达式。stateful(widget, state) 只接收两个参数:widget 和 state。旧的 $ / #(...) adapter 写法已经移除。
适配器适合简单桥接:
- 原生
Paragraph、Line、Block等无状态 widget。 - 原生
List、Table等需要 ratatui state 的 widget。
如果你需要在组件内部构造复杂 widget、持有 props、接事件或重写布局,就手写 Component。完整示例见 原生 Widget 桥接。
routes!
Section titled “routes!”routes! 复用 element! 的 props 头部语法:
圆括号 () 是路由组件 props;花括号 {} 是子路由。经 routes! 传入的 props 会进入 'static 路由表,不能借用栈上临时值。
动态数据不要塞进路由 props:
- 路径里的动态段用
use_params()。 - 导航时的轻量上下文用
push_with_state/try_use_route_state::<T>()。 - 跨页面业务状态用 Atom 或外层 Provider。
更多路由边界见 路由。