Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 183 additions & 1 deletion docs-src/0.6/src/reference/component_props.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ In some cases, you may wish to create a component that acts as a container for s
{{#include src/doc_examples/component_element_props.rs:Clickable}}
```

Then, when rendering the component, you can pass in the output of `rsx!{...}`:
Then, when rendering the component, you can pass in the output of `rsx!{...}`:

```rust, no_run
{{#include src/doc_examples/component_element_props.rs:Clickable_usage}}
Expand All @@ -151,3 +151,185 @@ DemoFrame {
component_children::App {}
}
```

## Reactive Props

In Dioxus, props are **the primary mechanism** for enabling communication between components. When passed down, they **trigger re-renders** upon changes, allowing your UI to reflect the latest application state. But what happens when props are used inside reactive systems such as `use_memo`, `use_future`, or `use_resource`? That's where **reactive props** become essential. Without making the prop reactive, those hooks **won't know** they need to update, leading to stale computations and inconsistent interfaces. Let's explore the principles of reactive props and how you can make them **predictable**, **efficient**, and **declarative**.

### Non-Reactive by Default

By default, primitive props like `f64`, `String`, etc., **aren't tracked** by hooks. They are copied in and won't trigger recomputation inside memoized or asynchronous logic.

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:Temperature}}
```

This works once. But if `celsius` changes from `30.0` to `32.0`, the UI updates the Celsius label but **not** the Fahrenheit conversion. The memo still uses the **stale value** `30.0`.

### Reactive via `ReadOnlySignal<T>`

Dioxus offers a built-in, elegant solution: the [`ReadOnlySignal<T>`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ReadOnlySignal.html). It's a lightweight, `Copy`able, `Clone`able, zero-cost abstraction for reactive reads.

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:ReactiveTemperature}}
```

The `use_memo` hook now **subscribes** to the signal. Whenever `celsius` changes, the memo is recalculated.

### `use_reactive!` for Precision Control

If you prefer to avoid signals, use [`use_reactive`](https://docs.rs/dioxus/latest/dioxus/prelude/macro.use_reactive.html) macro to **declare dependencies** manually inside hooks:

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:UseReactive}}
```

This gives you **deterministic control** over which variables should cause recomputation, useful for deeply nested computations or external state systems.

### Gotchas and Tips

- **Signals are both `Copy`able and `Clone`**able: You can pass `ReadOnlySignal<T>` by value, reference, or clone it. Internally, it's a lightweight handle to a reactive state. This makes it easy to lift signals into other hooks without worrying about ownership issues.

- **You can combine signals with the `use_reactive!` macro**: This is useful when only part of your dependencies are reactive. For example:

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:UseReactiveGotcha}}
```

- **Avoid using non-reactive props in `use_future` or `use_resource`**: These hooks only re-run when their dependencies change. If those dependencies are non-reactive (like `String` or `f64`), the hook won't re-execute when they update. Example of incorrect usage:

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:UseFutureGotcha}}
```

Correct approach using a reactive prop:

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:UseFutureCorrect}}
```

- **Memoized values from `use_memo` are reactive**: The result of `use_memo` is a value. If you need the result to drive other reactive state, store it in a memo:

```rust, no_run
// ✅ This is a reactive memo that will update when `count` changes
let doubled = use_memo(move || count() * 2);
```

- **`use_signal` vs. `use_memo`**: Use `use_signal` when the value will change due to internal component state or user interaction. Use `use_memo` when deriving a value from other reactive props or signals and you want automatic recomputation.

- **Signal updates are conditional on value changes**: If you call `.set()` on a signal with the same value it already holds, dependent effects and hooks will not re-run. If you need to force an update, consider mutating inner data if you're using something like `Signal<Vec<T>>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't accurate

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant by the first bullet point was the following:

#[component]
fn App() -> Element {
    let mut count = use_signal(|| 0);

    let double_count = use_memo(move || count() * 2);

    rsx! {
        div {
            h1 { "Count: {count}" }
            h2 { "Double Count (memoized): {double_count}" }

            button {
                onclick: move |_| {
                    count.set(count() + 1);
                },
                "Increment"
            }
        }
    }
}

In this example, count is a mutable signal. It's directly updated when the button is clicked (count.set()). double_count is derived from count. It's recomputed automatically whenever count changes.

By the second bullet point, I meant the following:

#[component]
fn App() -> Element {
    let mut count = use_signal(|| 0);

    let double_count = use_memo(move || {
        count() * 2
    });

    rsx! {
        div {
            h1 { "Count: {count}" }
            h2 { "Double Count (memoized): {double_count()}" }

            button {
                onclick: move |_| {
                    count.set(count() + 1);
                },
                "Increment"
            }

            button {
                onclick: move |_| {
                    count.set(count());
                },
                "Set Same"
            }
        }
    }
}

In this example, the increment button updates count, so double_count will recompute. The "Set Same" button calls .set() with the same value count(), so no reactivity will be triggered. the double_count won't double. This demonstrates that signals only trigger updates if their value changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment was meant for line 220 (the second point)

The memo doesn't rerun its subscribers unless the value changes, but the signal does. If you click the set same button in this example, you can see the log as the memo reruns:

use dioxus::prelude::*;

fn main() {
    dioxus::launch(app);
}

fn app() -> Element {
    let mut count = use_signal(|| 0);

    let double_count = use_memo(move || {
        println!("Calculating double count");
        count() * 2
    });

    rsx! {
        div {
            h1 { "Count: {count}" }
            h2 { "Double Count (memoized): {double_count()}" }

            button {
                onclick: move |_| {
                    count.set(count() + 1);
                },
                "Increment"
            }

            button {
                onclick: move |_| {
                    count.set(count());
                },
                "Set Same"
            }
        }
    }
}

Memos are already covered in the state guide. We don't need to duplicate that documentation here


- **Cloning signals in every render can be inefficient**: Although cloning signals is cheap, avoid doing it repeatedly inside `rsx!` or render loops. Instead, just read the signal:

```rust, no_run
rsx! {
div { "Value: {count()}" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clones the value inside the signal instead of the pointer, which is much more expensive

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant by this bullet point is that it's better to read the signal's value directly, rather than cloning it. For example:

div { "Value: {count()}" }

Instead of:

div { "Value: {*(count.clone()).read()}" }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on the value inside the signal, but generally, the second code snippet is cheaper.

This will clone the value inside the signal, not return a reference to it:

div { "Value: {count()}" }

This will clone the 32 byte signal and then return a reference to the inner value:

div { "Value: {*(count.clone()).read()}" }

If your value is larger than 32 bytes, the second code snippet will save cloning that value. I also don't see how this fits into the managing props reference. If this were true, it would be better to document this in the state guide where signals are explained

}
```

- **Closures can capture stale props if not handled properly**: If you capture a non-reactive prop in a closure inside `use_future` or `use_memo`, it won't reflect updates. Always make sure the closure references a signal or reactive value.

- **Debugging tip**: If a memo or effect isn't updating, log values or use visual indicators in the UI to confirm whether the input values are truly changing. In most cases, this is caused by a missing reactive dependency or an incorrectly captured closure.

Let's consider the following example

```rust, no_run
{{#include src/doc_examples/component_reactive_props.rs:GotchasApp}}
```

```inject-dioxus
DemoFrame {
component_reactive_props::App {}
}
```

This pattern ensures that **networked async hooks** properly respond to prop changes using signals.

## Extending Elements

Often when building reusable components, you want them to **act like native HTML elements**, passing arbitrary attributes like `style`, `onmouseenter`, or `disabled`. Dioxus allows this through the `#[props(extends = ...)]` attribute. This lets you **extend the props** of your component with either:

- All **global HTML attributes**
- Attributes specific to a particular HTML tag (e.g. `button`, `input`, etc.)

Let's consider the following example:

```rust, no_run
{{#include src/doc_examples/component_extending_elements.rs:ExtendingPanel}}
```

Usage:

```rust, no_run
{{#include src/doc_examples/component_extending_elements.rs:ExtendingPanelUsage}}
```

```inject-dioxus
DemoFrame {
component_extending_elements::App {}
}
```

No need to manually forward every attribute. Simply spread `..attrs`, and your component becomes fully HTML-compatible.

### Merging Multiple Sources

You can extend multiple attribute domains (e.g. global + tag-specific):

```rust, no_run
{{#include src/doc_examples/component_extending_elements.rs:ExtendingButton}}
```

```rust, no_run
{{#include src/doc_examples/component_extending_elements.rs:ExtendingButtonUsage}}
```

```inject-dioxus
DemoFrame {
component_extending_elements::ActionApp {}
}
```

Dioxus resolves conflicts internally. However, avoid combining elements with **incompatible attribute semantics**.

### Caveats and Tips

- **Attribute collision resolution**: If multiple values are passed for the same attribute (e.g. `class` appears in both props and `..attrs`), the **last occurrence wins**. This is standard HTML behavior, but it's worth watching out for when merging attributes from different sources.

- **Cannot override named props**: The `extends` system only covers HTML attributes. You **cannot override custom or named props** declared explicitly in the component signature. These must still be handled and destructured manually.

```rust, no_run
#[component]
fn Card(
title: String, // explicitly required named prop
#[props(extends = GlobalAttributes)]
attrs: Vec<Attribute>,
) -> Element {
rsx! {
div {
..attrs, // forwarded HTML attributes
h2 { "{title}" }, // uses the explicit prop
p { "This is a card." }
}
}
}
```

- **Avoid extending incompatible element types**: You must **only extend attribute sets that match the tag you're rendering**. Extending `input` attributes on a `div` may compile, but attributes like `type` or `value` will be meaningless or even silently dropped in the browser.

- **Prop filtering is manual**: If you want to restrict which attributes are allowed or filter out certain ones (e.g., to enforce accessibility or prevent accidental overrides), you must do this manually inside your component logic.
Comment on lines +318 to +320
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these two statements add anything. What would the alternative behavior be?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, in the first bullet point i was explaining what NOT to do:

#[component]
fn Bad(
    // Extending input attributes on a <div> (not meaningful!)
    #[props(extends = GlobalAttributes, extends = input)] attrs: Vec<Attribute>,
) -> Element {
    rsx! {
        div {
            // Attributes like `type` or `value` are not on <div>.
            ..attrs,
        }
    }
}

Instead, here is how you should do it correctly:

#[component]
fn Good(
    // Extending input attributes on an <input>.
    #[props(extends = GlobalAttributes, extends = input)] attrs: Vec<Attribute>,
) -> Element {
    rsx! {
        input {
            ..attrs,
        }
    }
}

This means that element-specific attributes must be extended on matching elements. Otherwise, attributes like type and value have no semantic meaning on a <div>. Browsers won't interpret them in any special way. So, Dioxus doesn't prevent you from rendering type="text" on a <div>, but you shouldn't because:

  • Browsers won't do anything with them on a <div>.
  • For semantic HTML, it's incorrect and can confuse other developers (or screen readers, accessibility tools, etc.).
  • Using <div type="text"> is not valid HTML (even though it renders).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statements are clear enough, but I'm struggling to think of a situation where this information would be useful. If you try to add type: "text" to a div, what would you expect to happen other than the attribute getting ignored? I guess the behavior here that might not be expected is that spread attributes are not type-checked

  • Prop filtering is manual: If you want to restrict which attributes are allowed or filter out certain ones (e.g., to enforce accessibility or prevent accidental overrides), you must do this manually inside your component logic.

I don't see any unexpected behavior here. If I have an API that returns Vec<usize>, it doesn't make sense to document that if you want even elements, you would need to manually filter the elements. That follows from the rust general docs.


- **Attribute order matters in practice**: While RSX generally follows a "last wins" policy, if you're conditionally overriding attributes (like `class`), you may want to control placement in the tree:

```rust, no_run
rsx! {
div {
class: "default",
..attrs, // if attrs contains a 'class', it will override "default"
}
}
```

- **Spreading is shallow**: `..attrs` simply forwards attribute key/value pairs. If you need to merge structured data (e.g., two `style` maps), you must handle it manually.

- **Debugging tip**: When using `..attrs`, it can be hard to trace where an unexpected attribute came from. Consider logging or printing props during development to confirm which attributes were forwarded.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this adds anything. The logging guide explains logging and we can assume people have basic debugging skills

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What i meant by the logging bullet point is the following:

#[component]
fn App() -> Element {
    rsx! {
        // Accidentally passing an unrelated attribute "data-unknown" to Button
        Button {
            title: "Hello World",
            class: "btn",
            id: "main-button",
            // Oops! This isn't intended
            "data-unknown": "???"
        }
    }
}

#[component]
fn Button(
    #[props(extends = GlobalAttributes, extends = button)] attrs: Vec<Attribute>,
) -> Element {
    // Log attributes to see what was forwarded
    tracing::info!("Button received attributes: {:?}", attrs);

    rsx! {
        button {
            ..attrs,
            "Action!"
        }
    }
}

When running the app, you can immediately see that data-unknown got forwarded. In a real-world app, this might hint that you're passing props that aren't actually supported by your component or the underlying DOM element.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement is clear, but we don't need to document the fact that you can log things for every API. There isn't anything special about logging spread attributes, signals, events, etc. It is true, but it just bloats the documentation, which makes it more difficult to skim through, search, and maintain

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments and the overall review. I learned some new things about signals/memos that I wasn't aware of. I'll take the above points into consideration and work on improving the documentation as a whole.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;

#[component]
pub fn App() -> Element {
// ANCHOR: ExtendingPanelUsage
rsx! {
Panel {
style: "border: 1px solid #ccc; padding: 1em;",
id: "main-panel",
class: "highlight"
}
}
// ANCHOR_END: ExtendingPanelUsage
}

// ANCHOR: ExtendingPanel
#[component]
fn Panel(#[props(extends = GlobalAttributes)] attrs: Vec<Attribute>) -> Element {
rsx! {
div {
..attrs,
"This is a panel"
}
}
}
// ANCHOR_END: ExtendingPanel

#[component]
pub fn ActionApp() -> Element {
// ANCHOR: ExtendingButtonUsage
rsx! {
ActionButton {
// TODO: File an issue to add this callback to `ActionButtonPropsBuilder<()>`
// cause button has `onclick` event handler and should be extended
// onclick: move |_| {},
disabled: true,
title: "Click to execute",
class: "btn"
}
}
// ANCHOR_END: ExtendingButtonUsage
}

// ANCHOR: ExtendingButton
#[component]
fn ActionButton(
#[props(extends = GlobalAttributes, extends = button)] attrs: Vec<Attribute>,
) -> Element {
rsx! {
button {
..attrs,
}
}
}
// ANCHOR_END: ExtendingButton
Loading