Unable to retrieve the hook that was initialized at this index
主线位置:更新主线里
run_scope调组件函数的那一步。每个use_*最后都落到Scope::use_hook,它只按「第几次调用」取值。序号和类型怎么决定取到什么,以及条件 hook 的三种基本情况,见 Scope 与 hooks 的「use_hook:只认序号」;本页讲循环和早返回,以及怎么在运行前查出来。
flowchart TB
hk["第 i 次调用 use_*"] --> has{"第 i 格存在?"}
has -->|否| push["追加新 hook"]
has -->|是| ty{"类型对得上?"}
ty -->|是| ret["返回这一格"]
ty -->|否| hp{"debug 且热补丁中?"}
hp -->|是| over["覆盖这一格"]
hp -->|否| pan["panic,组件变错误"]
本页实验在 M2 Max、macOS 27、rustc 1.99.0 上运行,依赖 dioxus = "=0.7.10",原生 VirtualDom;dx check 用的是 dx 0.7.9(git diff v0.7.9 v0.7.10 不涉及 packages/cli/src/check/)。
现象
pit1-exp/src/bin/hook_loop.rs。第一个组件给每个标签建一个「展开」开关,用的是循环里的 use_signal;第二个在两个 hook 之间早返回:
// one "expanded" flag per tag, created with a hook inside the loop
fn tag_list() -> Element {
let tags = TAGS.with(|t| t.borrow().clone());
let mut flags = Vec::new();
for _tag in &tags {
flags.push(use_signal(|| false));
}
let filter = use_signal(|| String::from("all"));
rsx! { "{tags.len()} tags, {flags.len()} flags, filter = {filter}" }
}
// an early return before the second hook
fn early_return() -> Element {
let count = use_signal(|| 1);
if EMPTY.get() {
return rsx! { "empty" };
}
let label = use_signal(|| String::from("items"));
rsx! { "{count} {label}" }
}
tag_list 先用 2 个标签渲染,再改成 1 个;early_return 在 EMPTY 为真、假之间来回切。每次改完 mark_dirty(ScopeId::APP),render_immediate 两次(第二次让错误边界渲染出来)。debug:
== hook in a loop
render 1 (2 tags) 2 tags, 2 flags, filter = all
thread 'main' (2102883) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-core-0.7.10/src/scope_context.rs:387:9:
Unable to retrieve the hook that was initialized at this index.
Consult the `rules of hooks` to understand how to use hooks properly.
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
Functions prefixed with "use" should never be called conditionally.
Help: Run `dx check` to look for check for some common hook errors.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
render 2 (1 tag) <div style="color:red;"><pre>Encountered panic: Any { .. }</pre></div>
== early return
render 1 1 items
render 2 (empty = true) empty
render 3 (empty = false) 1 items
render 4 (empty = true) empty
render 5 (empty = false) 1 items
release 除线程号外逐行相同。
- 循环:第一次渲染的 hook 表是
[bool, bool, String]。标签减到 1 个,第二次渲染里filter是第 2 次调用,拿到第 1 格的Signal<bool>,类型不对,panic。panic 被组件外的catch_unwind接住,整页变成根错误边界的Encountered panic。标签变多也一样:第 2 格原来是Signal<String>,现在要Signal<bool>。如果开关和filter恰好是同一类型,不会 panic,filter静默地拿到某个标签的开关值,这比 panic 更难查(Scope 与 hooks 的情况 B)。 - 早返回:完全正常。早返回只会让本次渲染少调用末尾的几个 hook,前面的序号不变;之后再走到后面,序号超出表长就是追加,再之后就是取回同一格。
let v = res.suspend()?;这种?也是早返回,放在其他 hook 之后同样没问题。
出处
panic 在 Scope::push_hook_value 的最后一个分支(第 387 行):序号在表内、downcast_ref 失败、又不在热补丁下。取值入口是 Scope::use_hook,它不记 hook 的名字和调用位置,所以 panic 只能说「这个序号」,说不出是哪一行。这也是为什么 panic 位置在 scope_context.rs 而不在你的代码里。
panic 文字里建议的 dx check 是 CLI 里一个基于 syn 的语法检查(packages/cli/src/check/mod.rs)。visit_expr_call 找名字以 use_ 开头的函数调用(is_hook_ident),沿着当前所在的语法结构往外看:在 if、match、for、while、loop、闭包、async 块里就报;所在函数返回 Element 算组件、名字以 use_ 开头算 hook,两者都不是就报「outside component or hook」。它不展开宏,rsx! 里的东西看不见。
把各种写法放进一个文件试(src/bin/hook_check.rs,只给 dx check 看):
fn in_if(show: bool) -> Element {
if show {
let _a = use_signal(|| 0);
}
rsx! {}
}
fn in_match(n: u8) -> Element {
match n {
0 => {
let _a = use_signal(|| 0);
}
_ => {}
}
rsx! {}
}
fn in_closure() -> Element {
rsx! {
button { onclick: move |_| { let _a = use_signal(|| 0); }, "x" }
}
}
fn after_early_return(empty: bool) -> Element {
if empty {
return rsx! {};
}
let _a = use_signal(|| 0);
rsx! {}
}
// not named use_*, so it does not look like a hook
fn make_state() -> Signal<i32> {
use_signal(|| 0)
}
fn helper_in_if(show: bool) -> Element {
if show {
let _a = make_state();
}
rsx! {}
}
dx check --bin hook_check -f src/bin/hook_check.rs(去掉了开头一行 Waiting for cargo-metadata 和 dx 与 dioxus 版本不一致的三行 ERROR,以及颜色):
1.66s INFO error: hook called conditionally: `use_signal` (inside `if`)
--> src/bin/hook_check.rs:6:14
|
6 | let _a = use_signal(|| 0);
| ^^^^^^^^^^
|
= note: `if show { … }` is the conditional
error: hook called conditionally: `use_signal` (inside `match`)
--> src/bin/hook_check.rs:14:16
|
14 | let _a = use_signal(|| 0);
| ^^^^^^^^^^
|
= note: `match n { … }` is the conditional
error: hook called outside component or hook: `use_signal`
--> src/bin/hook_check.rs:37:3
|
37 | use_signal(|| 0)
| ^^^^^^^^^^
ERROR dx check: error checking file
Caused by:
3 issues found.
if、match抓到了;早返回没报,和上面的运行结果一致。onclick闭包里的use_signal没报:它在rsx!里。make_state被报成「outside component or hook」,但真正有问题的helper_in_if没报:检查只看名字,make_state()不像 hook,它在if里也就不算条件 hook。给这类辅助函数起use_开头的名字,两处都会按规则检查。
对 hook_loop.rs 跑,报的是循环:
0.56s INFO error: hook called in a loop: `use_signal` (inside `for` loop)
--> src/bin/hook_loop.rs:16:16
|
16 | flags.push(use_signal(|| false));
| ^^^^^^^^^^
|
= note: `for _tag in &tags { … }` is the loop
修法
「每一项一份状态」交给子组件:子组件的 hook 在子组件自己的 scope 里,有几项就有几个 scope,hook 表各管各的。src/bin/hook_loop_fix.rs:
// each tag gets its own component, and the flag is that component's hook
#[component]
fn TagRow(tag: &'static str) -> Element {
let open = use_signal(|| false);
rsx! { "[{tag} open={open}] " }
}
fn tag_list() -> Element {
let tags = TAGS.with(|t| t.borrow().clone());
let filter = use_signal(|| String::from("all"));
rsx! {
for tag in tags {
TagRow { key: "{tag}", tag }
}
"filter = {filter}"
}
}
2 个、1 个、3 个标签依次渲染:
render 1 (2 tags) [rust open=false] [web open=false] filter = all
render 2 (1 tag) [rust open=false] filter = all
render 3 (3 tags) [rust open=false] [web open=false] [wasm open=false] filter = all
dx check --bin hook_loop_fix -f src/bin/hook_loop_fix.rs 输出 No issues found.,退出码 0;对原来的 hook_loop.rs 退出码是 1。
其他写法的改法:
- 条件 hook:hook 总是调用,条件放在用它的地方(
let s = use_signal(..); if show { .. s .. }),或者把条件分支整个做成子组件,用if show { Child {} }控制挂载。 - 一个 signal 装整组状态:
use_signal(HashSet::<TagId>::new)存展开的标签,比每项一个 signal 更容易和数据保持一致。子组件写法要给列表加 key,否则状态按位置走(本章第 6 篇)。
怎么确认没中招
- 在 CI 里跑
dx check,用退出码判断。不带-f时它读项目里每个.rs文件(按.gitignore过滤,不管模块是否被引用),在本实验 crate 上一次报出了上面两个文件的 4 处。它能抓if/match/ 循环 / 普通闭包 /async块里的use_*调用,以及在非组件、非use_函数里调 hook。 - 它抓不到的:
rsx!里的闭包、不以use_开头的辅助函数被条件调用。这些靠代码审查:hook 只在组件函数体最外层的语句里调用,在任何if/for/match/ 闭包之外。 - 运行时,类型不同的错位 panic 原文就是标题这句,位置固定是
scope_context.rs:387;类型相同的错位没有任何信号,只能靠上面两条预防。
小结
- hook 按调用序号取值,只校验类型。循环和条件让序号漂移:类型不同 panic、整页被错误边界替换,类型相同静默串值。
- 早返回(含
?)只截掉末尾的 hook,不违反规则。 dx check是基于语法的检查:能抓if/match/ 循环 / 闭包里的use_*,看不进rsx!,只认use_前缀。- 每项一份状态用子组件(加 key)或一个装整组状态的 signal。