Inconsistent component remounting behavior with dynamic key prop
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.
// 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.
-
Inline Component (
Scenario 1): TheCountercomponent'sinitialprop updates, but its internalcountstate is preserved. The hooks are not reset. -
Inline Component in
if true(Scenario 2): TheCountercomponent is fully remounted. Its internalcountstate is reset to the newinitialvalue. -
Pre-built VNode (
Scenario 3&4): TheCountercomponent 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.
The reason those examples behave differently is the key attribute only effects rendering inside of fragments. Outside of fragments (loops) the key attribute is ignored. 2, 3, and 4 desugar to fragments under the hood while 1 does not
Thanks for the explanation, that helps clarify the role of fragments and iterators.
I tried explicitly using a Fragment to see if it would enable the key to work on a single component, but neither of these approaches triggered the desired remount:
Fragment {
Counter { key: "{initial}", initial: initial() }
}
Fragment { key: "{initial}",
Counter { initial: initial() }
}
The workaround is to wrap the component in an inline rsx! call:
// This works as expected and remounts the component
{ rsx! {
Counter { key: "{initial}", initial: initial() }
} }
This leads to my main question: What is the idiomatic or recommended pattern for resetting a component's internal state when its props change? Is the { rsx! { Component { key: "...", ... } } } pattern the intended way to force a remount for a single component?
This leads to my main question: What is the idiomatic or recommended pattern for resetting a component's internal state when its props change? Is the
{ rsx! { Component { key: "...", ... } } }pattern the intended way to force a remount for a single component?
Currently yes, although this behavior is mostly a side effect of the key behavior. Keys work as I would expect for maintaining iterator state which is there main purpose, but have some odd quirks when you use them to reset state
Are there other mechanisms (other than keys) to reset state?