状态
Ratatui Kit 里的状态不是“都放进一个 store”。不同状态有不同生命周期:有些跟随组件实例,有些只属于一棵 Provider 子树,有些跟随一次导航,有些才是进程级共享。
想先看运行中的完整例子,可以从 Atom 全局状态 开始。这个页面则作为 reference,解释每种状态应该放在哪里,以及 State<T> / AtomState<T> 这些响应式句柄具体怎么工作。
| 状态 | 优先位置 | 生命周期 |
|---|---|---|
| 输入框草稿、列表游标、弹窗 open | use_state | 跟随组件实例 |
| 子树级主题、workspace、局部服务对象 | ContextProvider + use_state | 跟随 Provider 子树 |
| 一次导航携带的临时上下文 | RouteState | 跟随 history entry |
| 当前用户、全局主题、跨页面缓存 | Atom / use_atom | 跟随进程 |
状态越全局,越要谨慎。Atom 不是“更强的 State”,而是更长生命周期、更宽作用域的 State。
use_state
Section titled “use_state”use_state 创建组件局部响应式状态:
返回的 State<T> 是一个可复制句柄。你可以把它按值传进事件闭包、子组件 props、手写 Component,或异步任务里:
State<T> 要求 T: Unpin + Send + Sync + 'static。这个约束让状态句柄可以安全地进入后台任务,并在任务完成后唤醒 UI。
State<T> 和 AtomState<T> 都基于同一个 ReactiveHandle,读写 API 一致:
| API | 用途 | 失败行为 |
|---|---|---|
get() | 读取 Copy 值 | 借用冲突时 panic |
read() | 获取只读 borrow | 借用冲突时 panic |
try_read() | 尝试只读 borrow | 返回 None |
write() | 获取可变 borrow,并在变更后唤醒 | 借用冲突时 panic |
try_write() | 尝试可变 borrow | 返回 None |
set(value) | 覆盖值,并唤醒 | 借用冲突时静默跳过 |
write_no_update() | 可变 borrow,但不唤醒 | 借用冲突时 panic |
try_write_no_update() | 尝试可变 borrow,但不唤醒 | 返回 None |
set_no_update(value) | 覆盖值,但不唤醒 | 借用冲突时静默跳过 |
断言型 API 适合普通组件逻辑:借用冲突通常是编程错误,应该快速暴露。try_* 适合可选能力、回调里防御性读取,或者你明确知道当前可能被借用。
写入会在可变借用 drop 时唤醒渲染循环:
对于 Copy 数值,+=、-=、*=、/= 也会触发同样的变更通知:
no_update 写入
Section titled “no_update 写入”有一种写入不应该触发 UI 更新:draw 阶段把状态交给 ratatui 的 stateful widget 渲染。
这类写入只是 ratatui widget 的绘制协议需要 &mut State,不是业务状态变化。真正改变选中项的地方应该在事件 handler 里用 write()。完整例子见 原生 Widget 桥接。
try_write_no_update() 和 set_no_update() 是同一条路径的非 panic / 直接赋值版本。它们只适合绘制协议、测试 harness 或非常明确的内部同步场景;普通业务状态写入应该继续用 write() / set(),让界面能被唤醒。
Atom 是 feature-gated 能力,需要开启 atom 或 full。它适合保存进程级共享状态:主题、当前用户、跨页面缓存、全局选择结果。
Atom 是模块级声明,不需要宏:
组件内用 use_atom 订阅:
use_atom(&THEME) 返回 AtomState<T>。它和 State<T> 一样可读写、可复制、可传进闭包。区别在于唤醒模型:写入 Atom 会唤醒订阅了这个 Atom 的组件;没有订阅它的组件不会因为这次写入重绘。
组件外也可以直接读写 Atom:
get() 只适用于 T: Copy;非 Copy 值用 THEME.state().read()。
Atom 的订阅和退订
Section titled “Atom 的订阅和退订”use_atom 每帧都会把 hook 内部句柄校准到当前传入的 Atom。组件被卸载,或者同一个 hook 位置开始订阅另一个 Atom 时,旧 Atom 上以组件 key 注册的 waker 会被移除。
这意味着可以安全写这种“参数决定订阅哪个 Atom”的 hook,但仍然要保持 hook 调用顺序稳定:
不要手动维护 Atom 的订阅表;use_atom 会处理注册和退订。
草稿不要进 Atom
Section titled “草稿不要进 Atom”输入过程中的每个字符通常不该写进 Atom。以 Atom 全局状态 为例:
草稿属于当前编辑页;提交后的值才属于整个应用。把每个按键都写进 Atom,会让无关订阅者频繁醒来,也会让取消编辑变得别扭。
Context 不是 Atom
Section titled “Context 不是 Atom”如果一组数据只应该在某棵子树里可见,优先用 ContextProvider。常见例子包括局部主题、workspace 设置、页面级服务对象。
Context 的价值是“作用域清楚、可局部覆盖”。同一种类型可以在嵌套 Provider 里覆盖,子组件读取到最近那一层。完整例子见 自定义 Provider。
不要为了“传得远”把页面级状态默认升到 Atom。先问它有没有天然作用域;有的话,Provider 通常更清楚。
RouteState 不是 Atom
Section titled “RouteState 不是 Atom”Router 是 Provider 作用域状态,不是全局单例。RouteState 是单个 history entry 的可选 payload,适合从列表页进入详情页时携带一次性上下文:
普通 push(path) / replace(path) 会清空当前 route state;只有 push_with_state / replace_with_state 显式携带 state。这样可以避免旧详情页的 payload 泄漏到无关页面。
URL 必要信息放进 path params,一次性、非 URL 语义的上下文才放进 RouteState。更多细节见 路由。
| 问题 | 倾向 |
|---|---|
| 组件卸载后状态是否应该消失? | use_state |
| 是否只属于某个页面或子树? | ContextProvider + use_state |
| 是否只属于一次导航? | RouteState |
| 多个页面是否长期读写同一份业务数据? | Atom |
| 写入是否应该触发当前组件重绘? | write() / set() |
| 只是 draw 阶段满足 stateful widget 协议? | write_no_update() |
一个实用规则:先把状态放在它能正确工作的最小作用域里。只有当多个独立页面、组件或后台任务都需要同一份长期状态时,再把它升到 Atom。