DioxusLabs / DioxusLabs/dioxus

Inconsistent component remounting behavior with dynamic key prop

Open
#4,688 4 comments 0 reactions 0 assignees View on GitHub
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.