loading...
Cover image for Basic Interactions with Yew
Fullstack Frontend

Basic Interactions with Yew

kayis profile image K Updated on ・5 min read

Somehow the last article about Yew got more traction than I anticipated. Not much, but still!

That's why I sat down and played a bit around with Yew again, in the hope that one day a Yew maintainer will jump in and call me out on the Rust/Yew crap I'm teaching people here :D

I still hope to get a bit better at Rust, but I have to admit this framework made most of the things pretty easy. I didn't fight the borrow checker much until now. But I'm still in the "let's sprinkle some &s here and there"-stage without really understanding what's going on, haha.

In this article, we will build components with fundamental interactions. Again, nothing fancy, just a counter and a text-area. They don't even interact with each other.

TL;DR: The finished app 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 previous article.

Creating the Main Component

Like in the previous article, we are going to have three components. The first one is the actual app and lives in src/lib.rs. It has the following code:

#![recursion_limit="1000"]

mod counter;
use counter::Counter;

mod text;
use text::Text;

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

struct Model {}

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

    fn create(_props: 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! {
            <div>
                <Counter />
                <Counter />
                <Text />
                <Text />
            </div>
        }
    }
}

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

It doesn't do anything but rendering the other two components we will implement. Compared to the previous article, nothing new here.

Creating the Counter Component

The counter component displays a number that can be incremented and decremented; the 101 of UI interactions. Create a new file at src/counter.rs and add this code:

use yew::prelude::*;

pub struct Counter {
    link: ComponentLink<Self>,
    value: i64,
}

pub enum Msg {
    Increment,
    Decrement,
}

impl Component for Counter {
    type Message = Msg;
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            link,
            value: 0,
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Increment => self.value += 1,
            Msg::Decrement => self.value -= 1,
        }
        true
    }

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

    fn view(&self) -> Html {
        html! {
            <div>
              <button onclick=self.link.callback(|_| Msg::Increment)>{"+"}</button>
              <span style="width: 50px">{ self.value }</span>
              <button onclick=self.link.callback(|_| Msg::Decrement)>{"-"}</button>
            </div>
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

First, there is a struct that holds our state. It consists of a link and a value. The link is required to execute our component methods later, for example, when a button is clicked. The value is the number that can be updated later.

Second, there is an enum that defines messages. Messages can be used with the update method of our component to tell it what to do. In this case, we have two messages, Increment and Decrement.

We have to set the Message type of the Component trait to our Msg enum, so the type inferences work correctly (I guess?).

The create method sets the value to zero when a new component instance is created.

The update method is responsible for updating the state. It receives Increment or Decrement messages and acts accordingly on our value state.

The change method doesn't do anything because our component doesn't accept any properties from its parent.

The view gets called when the update method returned a true, which it always does. If we had some background state that isn't visible to the user, we could skip a render by returning a false from the update method.

In the view method, we can also see the use of the link state. The callback method of the link receives ... well ... a callback/closure. This callback has to return the message (Increment or Decrement) that we want to handle in the update method from above.

Overall, the whole thing isn't much different from React or Vue, just a bit more code required.

Creating the Text Component

The text component is a single textarea element that listens to its inputs and renders out upper-case versions of the text entered into it.

Create a src/text.rs file with this code:

use yew::prelude::*;

pub struct Text {
    link: ComponentLink<Self>,
    content: String,
}

pub enum Msg {
    Update(String),
}

impl Component for Text {
    type Message = Msg;
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self { link, content: "".to_string() }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::Update(content) => self.content = content.to_uppercase(),
        }
        true
    }

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

    fn view(&self) -> Html {
        html! {
            <textarea
                oninput=self.link.callback(|event: InputData| Msg::Update(event.value))
                value=&self.content>
            </textarea>
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This component is mostly the same as the last. The main difference is that it only one message, but this time the message has some data.

In this case, a String value. To get the data out of the textarea element and into our Msg::Update message, we have to look into the view method.

The oninput callback uses the event argument this time. It's of type InputData and comes with its fresh value. We can call Msg::Update(event.value) to wrap our new value with our message, and both will be on their way to the update method.

The update method uses match again to check which message we received. Still, this time it destructures the message so we can use the content (previously value) to update the state of our component; we also call to_uppercase on it, a method of String, to make it evident that the new text went through this method. Since we return true, the view method will be called by the framework after updating and rendering our new text.

Summary

You get an update/view loop going that is triggered by you clicking/typing things, same as with JavaScript/React. Since the components are encapsulated, I can do my thing in one component without looking into another.

Sure, Rust is much wordier than JavaScript, but then you get more helpful error messages. Also, enums and pattern matching is a very powerful tool that is missing in JavaScript and I could imagine in the long run worth the extra code to write.

I found this to be a good Rust exercise. I learned that I still doesn't understand the module system, let alone macros.

Discussion

pic
Editor guide