component_children.md 5.6 KB

Passing children and attributes

Often times, you'll want to wrap some important functionality around your state, not directly nested inside another component. In these cases, you'll want to pass elements and attributes into a component and let the component place them appropriately.

In this chapter, you'll learn about:

  • Passing elements into components
  • Passing attributes into components

The use case

Let's say you're building a user interface and want to make some part of it clickable to another website. You would normally start with the HTML <a> tag, like so:

rsx!(
    a {
        href: "https://google.com"
        "Link to google"
    }
)

But, what if we wanted to style our <a> tag? Or wrap it with some helper icon? We could abstract our RSX into its own component:

#[derive(Props)]
struct ClickableProps<'a> {
    href: &'a str,
    title: &'a str
}

fn clickable(cx: Scope<ClickableProps>) -> Element {
    cx.render(rsx!(
        a {
            href: "{cx.props.href}"
            "{cx.props.title}"
        }
    ))
}

And then use it in our code like so:

rsx!(
    clickable(
        href: "https://google.com"
        title: "Link to Google"
    )
)

Let's say we don't just want the text to be clickable, but we want another element, like an image, to be clickable. How do we implement that?

Passing children

If we want to pass an image into our component, we can just adjust our props and component to allow any Element.

#[derive(Props)]
struct ClickableProps<'a> {
    href: &'a str,
    body: Element<'a>
}

fn clickable(cx: Scope<ClickableProps>) -> Element {
    cx.render(rsx!(
        a {
            href: "{cx.props.href}"
            {&cx.props.body}
        }
    ))
}

Then, at the call site, we can render some nodes and pass them in:

rsx!(
    clickable(
        href: "https://google.com"
        body: cx.render(rsx!(
            img { src: "https://www.google.com/logos/doodles/2021/seasonal-holidays-2021-6753651837109324-6752733080595603-cst.gif" }
        ))
    )
)

This pattern can become tedious in some instances, so Dioxus actually performs an implicit conversion of any rsx calls inside components into Elements at the children field. This means you must explicitly declare if a component can take children.

#[derive(Props)]
struct ClickableProps<'a> {
    href: &'a str,
    children: Element<'a>
}

fn clickable(cx: Scope<ClickableProps>) -> Element {
    cx.render(rsx!(
        a {
            href: "{cx.props.href}"
            {&cx.props.children}
        }
    ))
}

And to call clickable:

rsx!(
    clickable(
        href: "https://google.com"
        img { src: "https://www.google.com/logos/doodles/2021/seasonal-holidays-2021-6753651837109324-6752733080595603-cst.gif" }
    )
)

Note: Passing children into components will break any memoization due to the associated lifetime.

While technically allowed, it's an antipattern to pass children more than once in a component and will probably break your app significantly.

However, because the Element is transparently a VNode, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format:

fn clickable(cx: Scope<ClickableProps>) -> Element {
    match cx.props.children {
        Some(VNode::Text(text)) => {
            // ...
        }
        _ => {
            // ...
        }
    }
}

Passing attributes

In the cases where you need to pass arbitrary element properties into a component - say to add more functionality to the <a> tag, Dioxus will accept any quoted fields. This is similar to adding arbitrary fields to regular elements using quotes.


rsx!(
    clickable(
        "class": "blue-button",
        "style": "background: red;"
    )
)

For a component to accept these attributes, you must add an attributes field to your component's properties. We can use the spread syntax to add these attributes to whatever nodes are in our component.

#[derive(Props)]
struct ClickableProps<'a> {
    attributes: Attributes<'a>
}

fn clickable(cx: Scope<ClickableProps>) -> Element {
    cx.render(rsx!(
        a { 
            ..{cx.props.attributes},
            "Any link, anywhere"
        }
    ))
}

The quoted escapes are a great way to make your components more flexible.

Passing handlers

Dioxus also provides some implicit conversions from listener attributes into an EventHandler for any field on components that starts with on. IE onclick, onhover, etc. For properties, we want to define our on fields as an event handler:

#[derive(Props)]
struct ClickableProps<'a> {
    onclick: EventHandler<'a, MouseEvent>
}

fn clickable(cx: Scope<ClickableProps>) -> Element {
    cx.render(rsx!(
        a { 
            onclick: move |evt| cx.props.onclick.call(evt)
        }
    ))
}

Then, we can attach a listener at the call site:

rsx!(
    clickable(
        onclick: move |_| log::info!("Clicked"),
    )
)

Currently, Dioxus does not support an arbitrary amount of listeners - they must be strongly typed in Properties. If you need this use case, you can pass in an element with these listeners, or dip down into the NodeFactory API.

Wrapping up

In this chapter, we learned:

  • How to pass arbitrary nodes through the tree
  • How the children field works on component properties
  • How the attributes field works on component properties
  • How to convert listeners into EventHandlers for components
  • How to extend any node with custom attributes and children

Next chapter, we'll talk about conditionally rendering parts of your user interface.