fix(code_runner): use_resolved_theme 从 use_effect 闭包提到 cfg 块顶层

dx check 报 4 处 'hook called in a closure: use_resolved_theme':
use_resolved_theme() 是 hook(内部 use_context),被错误地放在 use_effect 的
move || { ... } 闭包内调用。Dioxus 规则要求 hook 只能在组件渲染路径顶层调用。

修复:在两个 #[cfg(target_arch="wasm32")] 块顶层各调用一次 use_resolved_theme()
拿到 Memo<ResolvedTheme>,move 进下方 effect 闭包;闭包内改为调用 memo
(resolved_theme())读值。调用 memo 本身不是 hook(Memo 是 Copy + Fn),

读值的同时建立主题订阅——主题切换时 effect 仍会重跑,行为不变。
范式对齐 ThemeToggle(src/theme.rs:341)的正确用法。

影响 4 处:CodeMirror 挂载/主题同步 effect、xterm 挂载/主题同步 effect。
This commit is contained in:
xfy 2026-07-15 10:29:01 +08:00
parent 0b40105341
commit 50e64cd34c

View File

@ -126,6 +126,10 @@ pub fn CodeRunner(
let mut editor_handle: Signal<Option<codemirror_bridge::EditorHandle>> = let mut editor_handle: Signal<Option<codemirror_bridge::EditorHandle>> =
use_signal(|| None); use_signal(|| None);
// 在 cfg 块顶层(渲染路径)取主题 memomove 进下方 effect 闭包;闭包内只
// 调用 memoresolved())读值——调用 memo 本身不是 hookuse_resolved_theme
// 这个 hook 必须在组件体顶层调用,不能放进 use_effect 闭包dx check 会报)。
let resolved_theme = use_resolved_theme();
// 首次挂载:构造 closure + optionscreate 后存进 editor_handle。 // 首次挂载:构造 closure + optionscreate 后存进 editor_handle。
// 用 resolved() 读取主题作为初始值(同时订阅,但主题切换由下方独立 effect 处理)。 // 用 resolved() 读取主题作为初始值(同时订阅,但主题切换由下方独立 effect 处理)。
@ -146,8 +150,7 @@ pub fn CodeRunner(
// 但 EditorHandle 签名要求该闭包,传 no-op 满足生命周期。 // 但 EditorHandle 签名要求该闭包,传 no-op 满足生命周期。
let on_run_shortcut = Closure::new(|| {}); let on_run_shortcut = Closure::new(|| {});
let resolved = use_resolved_theme(); let theme_name = if resolved_theme() == ResolvedTheme::Dark {
let theme_name = if resolved() == ResolvedTheme::Dark {
"dark" "dark"
} else { } else {
"light" "light"
@ -186,7 +189,6 @@ pub fn CodeRunner(
// 期间跳过,让 VT 事件负责换肤;动画结束后此 effect 会因 resolved 信号变化重跑 // 期间跳过,让 VT 事件负责换肤;动画结束后此 effect 会因 resolved 信号变化重跑
// (此时 is-theme-transitioning 已移除),做一次幂等的兜底同步。 // (此时 is-theme-transitioning 已移除),做一次幂等的兜底同步。
use_effect(move || { use_effect(move || {
let r = use_resolved_theme();
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
let transitioning = web_sys::window() let transitioning = web_sys::window()
@ -200,7 +202,7 @@ pub fn CodeRunner(
} }
if let Some(h) = editor_handle.read().as_ref() { if let Some(h) = editor_handle.read().as_ref() {
h.instance() h.instance()
.set_theme(if r() == ResolvedTheme::Dark { .set_theme(if resolved_theme() == ResolvedTheme::Dark {
"dark" "dark"
} else { } else {
"light" "light"
@ -242,6 +244,10 @@ pub fn CodeRunner(
use crate::xterm_bridge; use crate::xterm_bridge;
use wasm_bindgen::closure::Closure; use wasm_bindgen::closure::Closure;
// 在 cfg 块顶层(渲染路径)取主题 memomove 进下方 effect 闭包;闭包内只
// 调用 memoresolved_theme())读值——调用 memo 本身不是 hook。
let resolved_theme = use_resolved_theme();
// 首次挂载:构造 onReady 闭包 + XtermOptionscreate 后存进 term_handle。 // 首次挂载:构造 onReady 闭包 + XtermOptionscreate 后存进 term_handle。
// 订阅 show_output输出区在 show_output 变 true用户点运行后才渲染进 DOM // 订阅 show_output输出区在 show_output 变 true用户点运行后才渲染进 DOM
// 容器此前不存在;读 show_output 建立订阅,使其变 true 时重跑本 effect 完成挂载。 // 容器此前不存在;读 show_output 建立订阅,使其变 true 时重跑本 effect 完成挂载。
@ -255,8 +261,7 @@ pub fn CodeRunner(
} }
let on_ready = Closure::new(|| {}); let on_ready = Closure::new(|| {});
let resolved = use_resolved_theme(); let theme_name = if resolved_theme() == ResolvedTheme::Dark {
let theme_name = if resolved() == ResolvedTheme::Dark {
"dark" "dark"
} else { } else {
"light" "light"
@ -278,7 +283,6 @@ pub fn CodeRunner(
// 主题切换时同步终端主题。 // 主题切换时同步终端主题。
// VT 动画期间跳过(同 CodeMirror 的 use_effect,见上方注释)。 // VT 动画期间跳过(同 CodeMirror 的 use_effect,见上方注释)。
use_effect(move || { use_effect(move || {
let r = use_resolved_theme();
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
let transitioning = web_sys::window() let transitioning = web_sys::window()
@ -292,7 +296,7 @@ pub fn CodeRunner(
} }
if let Some(h) = term_handle.read().as_ref() { if let Some(h) = term_handle.read().as_ref() {
h.instance() h.instance()
.set_theme(if r() == ResolvedTheme::Dark { .set_theme(if resolved_theme() == ResolvedTheme::Dark {
"dark" "dark"
} else { } else {
"light" "light"