DioxusLabs / DioxusLabs/dioxus
Inconsistent component remounting behavior with dynamic key prop
- Dominant language
- Rust
- Stars
- 39.1k
- Forks
- 1.9k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 4
Description
### Problem Description
The `key` prop on a component is expected to trigger a full remount (unmount and mount) when its value changes, thereby resetting all internal state like hooks (`use_signal`, `use_state`, etc.).
However, I've observed inconsistent behavior depending on how the component with the dynamic `key` is rendered. Specifically, when a component is defined inline directly within the `rsx!` macro, changing its `key` does **not** trigger a remount. But if the component's `VNode` is first created and stored in a variable, rendering that variable **does** correctly trigger the remount.
This behavior is confusing, undocumented, and contradicts the expected functionality of keys, making it difficult to reliably reset component state.
### Steps to Reproduce
The following minimal reproducible example demonstrates the inconsistency across several scenarios.
```rust
// main.rs
use dioxus::prelude::*;
fn main() {
launch(App);
}
// A simple component with its own internal state
#[component]
fn Counter(initial: u32) -> Element {
let mut count = use_signal(|| initial);
rsx! {
div {
p { "initial: {initial}, count: {count}" }
button { onclick: move |_| count += 1, "Increment" }
}
}
}
#[component]
fn App() -> Element {
let mut initial = use_signal(|| 0);
// Pre-build VNodes for testing
let counter = rsx! {Counter { initial: initial() }};
let counter_with_key = rsx! {Counter { key: "{initial}", initial: initial() }};
let counter_memo = use_memo(move || rsx! {Counter { initial: initial() }});
let counter_memo_with_key =
use_memo(move || rsx! {Counter { key: "{initial}", initial: initial() }});
rsx! {
h1 { "Key Remount Test" }
button {
onclick: move |_| initial += 1,
"Increment initial (changes key)"
}
p { "Current initial value: {initial}"}
hr {}
// --- SCENARIO 1: Inline component with key ---
h2 { "Inline component with key" }
// OBSERVED: Hooks are NOT reset when `initial` changes.
Counter { key: "{initial}", initial: initial() }
hr {}
// --- SCENARIO 2: Inline component with key inside an `if` block ---
h2 { "Inline component with key inside `if true`" }
// OBSERVED: Hooks ARE reset when `initial` changes.
if true {
Counter { key: "{initial}", initial: initial() }
}
hr{}
// --- SCENARIO 3: Render a pre-built VNode with key ---
h2 { "Render pre-built VNode with key" }
// OBSERVED: Hooks ARE reset when `initial` changes.
{counter_with_key}
hr{}
// --- SCENARIO 4: Render a memoized VNode with key ---
h2 { "Render memoized VNode with key" }
// OBSERVED: Hooks ARE reset when `initial` changes.
{counter_memo_with_key}
// --- Controls (without key) for comparison ---
hr{}
h2 { "Control: Render pre-built VNode without key" }
// OBSERVED: Hooks are NOT reset.
{counter}
hr{}
h2 { "Control: Render memoized VNode without key" }
// OBSERVED: Hooks are NOT reset.
{counter_memo}
}
}
```
### Observed Behavior
When clicking the "Increment initial" button, the `initial` value and thus the `key` change.
1. **Inline Component (`Scenario 1`)**: The `Counter` component's `initial` prop updates, but its internal `count` state is preserved. The hooks are not reset.
2. **Inline Component in `if true` (`Scenario 2`)**: The `Counter` component is fully remounted. Its internal `count` state is reset to the new `initial` value.
3. **Pre-built VNode (`Scenario 3` & `4`)**: The `Counter` component is fully remounted, and its state is reset as expected.
Beyond that, I don't have much web development experience. If I want to reset the state of a signal when props change, what's the best practice? The lifecycle of `use_effect` doesn't seem to meet this requirement.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.