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 a clickable link 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/..." }
        ))
    }
)

Auto Conversion of the Children field

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
        }
    ))
}

Now, whenever we use Clickable in another component, we don't need to call render on child nodes - it will happen automatically!

rsx!(
    Clickable {
        href: "https://google.com"
        img { src: "https://www.google.com/logos/doodles/...." }
    }
)

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 cause your app to crash.

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 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