-
Notifications
You must be signed in to change notification settings - Fork 188
fix: add missing component macro documentation
#495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}} | ||
|
|
@@ -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>>`. | ||
|
|
||
| - **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()}" } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()}" }
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
I don't see any unexpected behavior here. If I have an API that returns |
||
|
|
||
| - **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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't accurate
There was a problem hiding this comment.
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:
In this example,
countis a mutable signal. It's directly updated when the button is clicked (count.set()).double_countis derived from count. It's recomputed automatically whenever count changes.By the second bullet point, I meant the following:
In this example, the increment button updates
count, sodouble_countwill recompute. The "Set Same" button calls.set()with the same valuecount(), so no reactivity will be triggered. thedouble_countwon't double. This demonstrates that signals only trigger updates if their value changes.There was a problem hiding this comment.
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:
Memos are already covered in the state guide. We don't need to duplicate that documentation here