Props and Nested Components with Yew

K - Nov 29 '20 - - Dev Community

Until now, we learned how to create components and how to implement basic interactions for them, but to wield the full power of a component system, we need to pass props down from parent to child components. Nesting of components into each other is also crucial.

Since no Yew dev came to question my methods, I'll see this as a sign that I'm not teaching you, and, in turn, myself crap. :D

TL;DR Again, the code can be found on GitHub

Setup

I won't go into installing Rust, wasm_bindgen, or setting up a Yew project. All that can be found in the first article of this series.

Creating the Main Component

Let's start with the root component of our app. Write the following code into your src/lib.rs file:

#![recursion_limit = "1000"]

mod list;

use list::ListGroup;
use list::ListGroupItem;

use wasm_bindgen::prelude::*;
use yew::prelude::*;

struct Model {}

impl Component for Model {
    type Message = ();
    type Properties = ();

    fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self {
        Self {}
    }

    fn update(&mut self, _msg: Self::Message) -> ShouldRender {
        false
    }

    fn change(&mut self, _props: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        html! {
            <ListGroup>
                <ListGroupItem>{"First"}</ListGroupItem>
                <ListGroupItem active=true>{"Second"}</ListGroupItem>
                <ListGroupItem>{"Third"}</ListGroupItem>
            </ListGroup>
        }
    }
}

#[wasm_bindgen(start)]
pub fn run_app() {
    App::<Model>::new().mount_to_body();
}
Enter fullscreen mode Exit fullscreen mode

If you read the previous article, most of that code should seem familiar, and if you have used any of the major frontend frameworks lately, even the new code in the view method doesn't seem any special. Only the text-nodes may seem a bit out of place with their extra syntax, but somehow Rust seems to have "opinions" about strings, haha.

Anyway, at the top, we declare a new module list.

The mod list statement tells Rust there is a module in a file called list.rs, which is in the same directory as the current file.

The use statement allows us to use the public values inside that module as defined in the current file.

Creating the List Component

Next, we need to implement the components we used in the root component. For this, let's create a src/list.rs file and insert this code:

use yew::prelude::*;

pub struct ListGroup {
    props: ListGroupProps,
}

#[derive(Properties, Clone)]
pub struct ListGroupProps {
    pub children: Children,
}

impl Component for ListGroup {
    type Message = ();
    type Properties = ListGroupProps;

    fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
        Self { props }
    }

    fn update(&mut self, _msg: Self::Message) -> ShouldRender {
        false
    }

    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        self.props = props;
        true
    }

    fn view(&self) -> Html {
        html! {
            <ul class="list-group">{ self.props.children.clone() }</ul>
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So what's going on here?

First, the struct that holds our components state has a props field. This field can be used to access the props later and also to compare new props to the old ones when we want to safe performance on re-renders.

Then the struct that actually holds our props. In this case, we only accept children as props, nothing else. The attribute above the struct definition here gives the struct some extra features that wouldn't be available in a plain struct. These features are required that Yew recognizes the struct as a props struct.

Next comes the trait implementation. We need to set the Properties type to the props struct we defined and then implement a few methods.

The first interesting method is create, where we save the received props into our state.

Then the change method, where we do the same. This time we could check if the props actually changed and return false if they didn't. Doing so would prevent a re-render.

Finally, the view method. Here we get our state struct as self. Since state struct holds our props and, in turn, our props hold children, we can call clone() on them to get a copy of our children that will be rendered into an unsorted list.

Creating the Item Component

Now we need the actual list items our list should hold. Since the list and the item component belong together, let's put them in the same file. Add this code at the bottom of the src/list.rs file:

pub struct ListGroupItem {
    props: ListGroupItemProps,
}

#[derive(Properties, Clone)]
pub struct ListGroupItemProps {
    pub children: Children,
    #[prop_or(false)]
    pub active: bool,
}

impl Component for ListGroupItem {
    type Message = ();
    type Properties = ListGroupItemProps;

    fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
        Self { props }
    }

    fn update(&mut self, _msg: Self::Message) -> ShouldRender {
        false
    }

    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        self.props = props;
        true
    }

    fn view(&self) -> Html {
        let mut classes = vec!["list-group-item"];

        if self.props.active {
            classes.push("active")
        }

        html! {
            <li class=classes>{ self.props.children.clone() }</li>
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The item component isn't much different from the list component. It renders to a list item HTML element and passes its children along.

The only extra is an active prop. This allows marking the component with different styling.

All props have to be marked public because the users of our components need to know what type every prop has.

With the #[prop_or(false)] attribute we can give every prop a default value. If you forget to do this and don't pass that value, you will get bizarre error messages with nothing to do with the missing value. So "YAY" for type-systems :D

In the view method, we use the active prop to conditionally concatenate a vector that will serve as our CSS class definition.

Yew accepts strings, vectors, and tuples as values for class.

Building & Testing the App

To build the app, run this command:

$ wasm-pack build --target web --out-name wasm --out-dir ./static
Enter fullscreen mode Exit fullscreen mode

If you followed the instructions of the first tutorial, you should now be able to run:

$ miniserve ./static
Enter fullscreen mode Exit fullscreen mode

And check out your app in the browser!

Typing Children

You're probably thinking, why can't we type the children props more specific?

Well, we can!

Instead of using the catch-all type Children we use the generic type ChildrenWithProps<T> where T is the item type we use.

So we have to refactor the props type of our list as follows:

#[derive(Properties, Clone)]
pub struct ListGroupProps {
    pub children: ChildrenWithProps<ListGroupItem>,
}
Enter fullscreen mode Exit fullscreen mode

This way, our list component only accepts a list of our item types as children and the compiler will warn the users if they used the wrong type.

Bonus: Binary Size Optimization

The WASM binary created is rather big; on my machine, it was ~130KB. So, I thought, let's look into some improvements here.

If you add a release profile to the bottom of the Cargo.toml file, things should shrink a bit.

[profile.release]
panic = 'abort' 
codegen-units = 1
opt-level = 'z' 
lto = true
Enter fullscreen mode Exit fullscreen mode

You can also swap out the memory allocator. There is one created explicitly for the WASM compile target and is smaller than the default one. This allocator is called wee_alloc and can be installed by adding it as a dependency to your Cargo.toml.

[dependencies]
yew = "0.17"
wasm-bindgen = "0.2.67"
wee_alloc = "0.4.2"
Enter fullscreen mode Exit fullscreen mode

And activating it at the top of your src/lib.rs file.

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Enter fullscreen mode Exit fullscreen mode

After these changes, I got the build down to ~100KB, which is a reduction of about 25%, not great, not terrible.

Conclusion

Rust still feels rather chatty, but overall at least Yew seems very familiar for a React dev like me.

Sometimes, the compiler's errors lead you in a completely false direction, but that's not often the case.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .