From 2498cd9c0bfb6acf6038ee7b083884263ee1f282 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 9 Jul 2026 16:43:33 +0800 Subject: [PATCH] =?UTF-8?q?docs(pitfall):=20=E8=AE=B0=E5=BD=95=E5=8F=8D?= =?UTF-8?q?=E5=BA=94=E5=BC=8F=20hook=20=E4=B8=8D=E8=BF=BD=E8=B8=AA?= =?UTF-8?q?=E6=99=AE=E9=80=9A=20prop=20=E7=9A=84=E9=99=B7=E9=98=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将本轮三轮调试(commits 79978aa / 1c56fd8 / f86cb48)踩到的同一 Dioxus 0.7 陷阱写入 Pitfall Log,避免后续重复踩坑: use_server_future / use_resource / use_memo 只在闭包内读取的 signal 变化时 重跑/重算(经 ReactiveContext 订阅)。普通组件 prop(String/i32 等非 signal) 被 move 进闭包后是冻结快照,读取它不建立订阅——prop 变化时 hook 不刷新, 组件虽重渲染但 hook 永久保留首次值。 该陷阱只在「同路由变体间导航」(/post/a → /post/b)触发:复用组件实例、仅 更新 props;跨变体导航会新挂载组件、hook 首次运行,掩盖问题。 记录两个真实案例(路由取数 use_server_future、正文解析 use_memo)的根因与 修复,给出判断规则(signal 依赖安全 / prop 依赖会静默过期 / prop 派生值 优先内联而非 use_memo),并标注 CommentSection 依赖 article key remount 才安全的潜在隐患。 --- AGENTS.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index a6e61ae..719f147 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -302,3 +302,22 @@ The single most repeated mistake: running only `cargo build --all-features` (ser ### Placing util functions: check the feature gate of the target module `src/utils/text.rs` is `#[cfg(feature = "server")]`-gated and pulls in `regex` (an optional, server-only dep). A frontend-reachable function (e.g. `escape_html`, called from a `#[component]`) **cannot** move there — the WASM build would fail on the missing `regex` crate. Use an ungated module (`src/utils/html.rs`, `src/utils/time.rs`) for code that must compile on both targets. Verify the destination module's cfg before moving anything into it. + +### Reactive hooks don't track plain props (`use_server_future` / `use_resource` / `use_memo`) + +Dioxus 0.7's reactive hooks (`use_server_future`, `use_resource`, `use_memo`) only re-run/recompute when a **signal read inside their closure** changes. They track dependencies via a `ReactiveContext` that subscribes to signals *during the closure's execution*. **A plain component prop (`String`, `i32`, any non-signal) is a frozen snapshot the moment it's `move`d into the closure — reading it establishes no subscription.** So when the prop later changes (e.g. a route param updates), the hook does **not** re-run. The component re-renders (props are unequal), but the hook's task/memo keeps its first-run value forever. + +This bites specifically on **same-route-variant navigation**: `/post/a → /post/b` (both `Route::PostDetail`) reuses the component instance and only updates props, so the frozen prop never refreshes the hook. Cross-variant navigation (`/ → /page/2`, i.e. `Route::Home → Route::HomePage`) mounts a fresh component and runs the hook for the first time, so it looks fine and hides the bug. + +This cost three rounds of debugging (commits `79978aa`, `1c56fd8`, `f86cb48`): + +1. **`use_server_future` not re-fetching** (`src/pages/post_detail.rs`, `home.rs`, `tags.rs`): `use_server_future(move || get_post_by_slug(slug.clone()))` — clicking prev/next changed the URL but the article content stayed on the old post (only a full browser refresh worked). Fix: read the slug **inside** the closure via `dioxus::router::router().current::()` — `current()` calls `subscribe_to_current_context()`, registering the subscription in the hook's `ReactiveContext`, so route changes re-run the future. + +2. **`use_memo` caching stale derived data** (`src/components/post/post_content.rs`): `use_memo(move || split_content_fragments(&content_html))` — after the route fix made the *title* update, the *body* still showed the old article. The memo permanently cached the first parse because `content_html` is a plain `String` prop. Fix: drop the memo, call the pure `split_content_fragments(&content_html)` directly in the render body (render purity is preserved — it's a pure function call). + +**Decision rule:** +- If the value a hook depends on is a **signal** (from `use_signal`, a `Resource`/`Memo`, `router.current()`, context), the hook re-runs correctly — no action needed. +- If it's a **plain prop** that can change while the component instance is reused (route params, parent-passed `String`/`i32`), the hook will **silently keep stale data**. Either read a signal source inside the closure, or drop the hook and recompute inline. +- For one-off derived values from props, prefer **direct inline computation** in the render body (it's pure) over `use_memo` — the memo buys nothing for prop-derived data since it won't recompute anyway, and it introduces this exact staleness bug. + +**Suspect areas still open:** `CommentSection` (`src/components/comments/section.rs:86`) reads `post_id` (plain prop) inside `use_resource` — currently safe only because its parent `article` uses `key: "{post.slug}"` and remounts it on every article switch. If that key is ever removed/changed, comments would stop refreshing on navigation.