components.md 7.0 KB

Introduction to Components

In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components.

In this chapter, we'll learn:

  • What makes a Component
  • How to model a component and its properties in Dioxus
  • How to "think declaratively"

What is a component?

In short, a component is a special function that takes input properties and outputs an Element. Typically, Components serve a single purpose: group functionality of a User Interface. Much like a function encapsulates some specific computation task, a Component encapsulates some specific rendering task.

Learning through prior art

Let's take a look at a post on r/rust and see if we can sketch out a component representation.

Reddit Post

This component has a bunch of important information:

  • The score
  • The number of comments
  • How long ago it was posted
  • The url short address
  • The title
  • The username of the original poster

If we wanted to sketch out these requirements in Rust, we would start with a struct:

struct PostData {
    score: i32,
    comment_count: u32,
    post_time: Instant,
    url: String,
    title: String,
    original_poster_name: String
}

If we look at the layout of the component, we notice quite a few buttons and functionality:

  • Upvote/Downvote
  • View comments
  • Share
  • Save
  • Hide
  • Give award
  • Report
  • Crosspost
  • Filter by site
  • View article
  • Visit user

If we included all this functionality in one rsx! call, it would be huge! Instead, let's break the post down into some core pieces:

Post as Component

  • VoteButton: Upvote/Downvote
  • TitleCard: Title, Filter-By-Url
  • MetaCard: Original Poster, Time Submitted
  • ActionCard: View comments, Share, Save, Hide, Give award, Report, Crosspost

Modeling with Dioxus

We can start by sketching out the Element hierarchy using Dioxus. In general, our "Post" component will be comprised of the four sub-components listed above. First, let's define our Post component.

Unlike normal functions, Dioxus components must explicitly define a single struct to contain all the inputs. These are commonly called "Properties" (props). Our component will be a combination of these properties and a function to render them.

Our props must implement the Props trait and - if the component does not borrow any data - PartialEq. Both of these can be done automatically through derive macros:

#[derive(Props, PartialEq)]
struct PostProps {
    id: Uuid,
    score: i32,
    comment_count: u32,
    post_time: Instant,
    url: String,
    title: String,
    original_poster: String
}

And our render function:

fn Post((cx, props): Scope<PostProps>) -> Element {
    cx.render(rsx!{
        div { class: "post-container"
            VoteButton {
                score: props.score,
            }
            TitleCard {
                title: props.title,
                url: props.url,
            }
            MetaCard {
                original_poster: props.original_poster,
                post_time: props.post_time,
            }
            ActionCard {
                post_id: props.id
            }
        }
    })
}

When declaring a component in rsx!, we can pass in properties using the traditional Rust struct syntax. Dioxus will automatically call "into" on the property fields, cloning when necessary. Our Post component is simply a collection of smaller components wrapped together in a single container.

Let's take a look at the VoteButton component. For now, we won't include any interactivity - just the rendering the score and buttons to the screen.

Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a tuple of Context and &Props and return an Element.

As covered before, we'll build our User Interface with the rsx! macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling cx.render converts our "lazy" rsx! structure into an Element.

#[derive(PartialEq, Props)]
struct VoteButtonProps {
    score: i32
}

fn VoteButton((cx, props): Scope<VoteButtonProps>) -> Element {
    cx.render(rsx!{
        div { class: "votebutton"
            div { class: "arrow up" }
            div { class: "score", "{props.score}"}
            div { class: "arrow down" }
        }
    })
}

Borrowing

You can avoid clones using borrowed component syntax. For example, let's say we passed the TitleCard title as an &str instead of String. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling clone on Rc<str>.

Because we're working in Rust, we can choose to either use Rc<str>, clone Title on every re-render of Post, or simply borrow it. In most cases, you'll just want to let Title be cloned.

To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output Element borrows from the component's props.

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

fn TitleCard<'a>((cx, props): Scope<'a, TitleCardProps>) -> Element<'a> {
    cx.render(rsx!{
        h1 { "{props.title}" }
    })
}   

For users of React: Dioxus knows not to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-'static borrow.

This means that during the render process, a newer version of TitleCardProps will never be compared with a previous version, saving some clock cycles.

The Context object

Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a Context parameter in the component declaration.

Have you ever wondered how the useState() call works in React without a this object to actually store the state?

React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs.

function Component(props) {
    let [state, set_state] = useState(10);
}

Because Dioxus needs to work with the rules of Rust it uses the Context object to maintain some internal bookkeeping. That's what the Context object is: a place for the component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the Context object to build robust and performant extensions for Dioxus.

fn Post((cx /* <-- our Context object*/, props): Scope<PostProps>) -> Element {
    cx.render(rsx!{ })
}

Moving forward

Next chapter, we'll talk about composing Elements and Components across files to build a larger Dioxus App.

For more references on components, make sure to check out:

  • [Components in depth]()
  • [Lifecycles]()
  • [The Context object]()
  • [Optional Prop fields]()