DEV Community

loading...
Cover image for YEW Tutorial: 01 Introduction

YEW Tutorial: 01 Introduction

davidedelpapa profile image Davide Del Papa Updated on ・7 min read

Yes, I'm humbling joining the fight to widespread Rust, with a tutorial on Yew.

Code to follow this tutorial

The code has been tagged with the relative tutorial and part.

git clone https://github.com/davidedelpapa/yew-tutorial.git
cd yew-tutorial
git checkout tags/v1

Part 1: Prerequisites, AKA, installation first

This tutorial pre-se does require at least some Rust knowledge.
However, I hope it would be profitable also for the beginners. I myself am still learning the language, yet in order to enjoy the full potential of Rust as applied to WASM technology one does not need to know all the intricacies of the type system.
A sufficient Rust knowledge is gained after few weeks of acquaintance with this language, thus I think that once the basics are learned, this guide can be very profitable also to the novices.

Of course, Rust itself must be installed. Next, we need to install wasm-pack. NOTE: I made this tutorial in a highly opinonated manner, not "just because", but just because the novices would not get lost around the many choices available.

cargo install wasm-pack

After this we'll need a JS bundler, and we'll use rollup.

npm install --global rollup

Now with thsese tools under our belt, let's move one to build soemthing.

Part 2: Let's create our first app from scratch

From our project's folder let's create a new rust project

cargo new yew-tutorial --lib

It's important that it be a lib

cd yew-tutorial
code .

I'm using code to develop, but you can use whichever IDE or plain text editor you like.
I myself many times use gedit, that simple!

Let's get over all files needed for this project, creating those missing as we go

cargo.toml

In cargo.toml let's add the following lines to the [dependencies]section

wasm-bindgen = "^0.2"
yew = "0.12"

We need to add as well a new [lib] section,
with the following:

[lib]
crate-type = ["cdylib"]

The file content should look like the following:

[package]
name = "yew-tutorial"
version = "0.1.0"
authors = ["you"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "^0.2"
yew = "0.12"

index.html

Let's add a barebones index.html in order to serve as the server's starting point:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Yew Tutorial</title>

    <meta charset="utf-8" />
    <script src="/pkg/bundle.js" defer></script>
  </head>

  <body></body>
</html>

The script we are refencing to, bundle.js, will be available in the pkg/ folder once the Rust lib is compiled. More on that later on.

We have left the <body> tag empty, because we'll nest there our Yew code

Now let's move on to change the files inside the scr folder!

scr/lib.rs

For each new lib project, cargo creates a lib.rs with a #[cfg(test)] section for testing.
It is very useful, but for now we'll get rid of it. In its place we'll put the following:

mod app;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
    yew::start_app::<app::App>();

    Ok(())
}

Let's explain it step-by-step:

  • we start defining the module app, which we'll create later on (file app.rs)
  • we use the wasm_bindgen::prelude::* in order to make use of the yew::start_app
  • the entrypoint for the WASM must be annotated, with a #[wasm_bindgen] annotation. We will refer to it in the main.js file we will create, and available to us upon bundling. More on that later.
  • as the run_app() can return a JsValue wrapped in an Option, we need to specify it as the function return type, and also to return Ok(())

Lets create our app mod next.

scr/app.rs

As we defind a mod called app, we need to create a app.rs file. Note: it is customary in the fullstack web world to call the main entry point of all web apps simply app. We as opinionated writers, will not renounce to this opinion!

Let's start our app by using the usual yew::prelude::*

use yew::prelude::*;

As customary in Rust, yew uses structs and enums to handle data related logic.

We will need one struct for handling the app, and an enum to control the messages sent to the app.

First of all, let's write down the struct App that we will implement, with the a custom state for this app.

pub struct App {
    counter: i64,
    link: ComponentLink<Self>,
}

As we can see the state is mantained by counter, while link is a ComponentLink which will contain the link to refer back to the current page invoking it. More on that later.

Now we'll write the enum Msg to implement the "form" logic, or controller logic, of the app.

pub enum Msg {
    AddOne,
}

Let's understand the logic behind the simple example we'll use (taken from yew's docs home page).

The app we will create will present a counter and a button to increase it:
the button is like a form, in that it will link back to the page sending a message, contained in the Msg enum. Since the message is simple enough it, is sufficient we'll send the enum with the appropriate message, in this case it is present only the message to "add one to the counter", i.e., AddOne.

When the app will receive the message it will increment by one its counter, that is, the counter state of the App struct. after this, it will render the change.

It seems all complicated, but it is easier in code than explaining it.

Without further ado, we'll implement the App struct:

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

We implement Yew's Component for our struct App.
In Yew each component has got a Message, used to comunicate with it, and a set of Properties.
For now we'll assign the enum Msg to be the type for Message.

Each component has three fucntions: create, update, and view.

fn create

create is the equivalent of a constructor for the component.

We'll use the return-type Self which basically means we have to return a App struct, that is the struct we are implementing.

    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
        App {
          link,
          counter: 0
        }
    }

We start with the counter set to zero.

It is good to notice that create() will get a Properties, which we will not use, hence the undersocre, and a ComponentLink to refer to itself, which we will store in the App struct we are returning.

fn update

udate determines the logic needed in order to handle events.

The update return type is ShouldRender which is a bool indicating if after the logic processing is done the component should render itself again or not. Returning a true or false is sufficient.

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

We match over the Message we receive (that is the only type it will receive), and establish that if the message is _AddOne, then it must add 1 to the internal counter.

At the end we return a true menaing that we vant to re-render the component

fn view

Finally we will "see" what our component looks like!
view is incharged of (re-)rendering our component in the DOM.

We can return various types, in this case it is a Html object.

    fn view(&self) -> Html {
        html! {
            <div>
            <p> {"Counter: "} { self.counter }</p>
                <button onclick=self.link.callback(|_| Msg::AddOne)>{ "Add 1" }</button>
            </div>
        }
    }

Notice first of all that the view relies only on its internal state: messages should be left where messages belong, that is, the updating logic. This is so, in order to separate the component's representation from its logic.

We'll use the html! powerful macro in order to render components in a quasi-html style.
The rationale behind it is the same as using JSX in React: inserting elements of HTML inside a language (Rust in this case), using a kind of DSL (Domain Specific Language).
Thus, we've used <div>, <button>, and <p> inside a Rust macro.

Some rules we can see:

  • the value of a Rust expression must be inserted inside brackets {}
  • text must be inserted as strings, so it is an expression, thus it belongs inside brackets as well
  • we have self to refer to the App's state.

One thing of interest: using the link to the component we activate a callback for the <button>'s onclink, to which we pass the message AddOne

Our app.rs is done!

main.js

In the root directory, let's create a main.js file, which will be the starting point for our wasm-js integration

import init, { run_app } from "./pkg/yew_tutorial.js";
async function main() {
  await init("/pkg/yew_tutorial_bg.wasm");
  run_app();
}
main();

We are calling here the .js and .wasm files that will be created in the directory pkg/ upon compilation. Convention wants that the names that we put in Rust, which are separated with dashes, become separated with undersocores in javascript. Thus our yew-tutorial becomes yew_tutorial.

The run_app() function will be available because it is exported from the lib.rs

Build and Run

It is now time to build and to run our first example

wasm-pack build --target web

The first build is usually longer than the following.

At the end of this we sould be greeted by a:

[INFO]: :-) Your wasm pkg is ready to publish at ./pkg.

If we check out the content we'll see the .js and .wasm files almost ready to be served.

Almost, because we need to bundle them first. We'll use the rollup tool we installed at the begnning

rollup ./main.js --format iife --file ./pkg/bundle.js

Let's serve it with our handy python http.server module.
Remember, ctrl-c in order to shut it down:

python -m http.server 8080

Now let's head our browser to http://0.0.0.0:8080/

We can see the value starting from 0 and a button to add to that value.

Our proud counter

Mission accomplished!

We will continue in the next installment of this tutorial talking about how to extend this simple example.

Discussion (7)

pic
Editor guide
Collapse
ajinkyax profile image
Ajinkya Borade

I feel use yew::prelude::*; should be introduced when we create app.rs to stop compile errors if someone like me is writing down code as we proceed.

And thanks a ton for making this a beautiful series :) going to follow you

Collapse
davidedelpapa profile image
Davide Del Papa Author

Yes, that in fact is now the trend for the Rust crates writers: it is now common to leave a ::prelude containing a small subset of the imports usually needed to write out programs for the common use-cases.
It is a very beginners friendly practice, but it is useful also for the more seasoned programmers.
In any case, rustc alerts you of the unused imports, so feel free to add 'use's when you are trying things out, and then clean the code once the program works.

Collapse
danielcardonarojas profile image
Daniel Cardona Rojas

I'm just starting out following up with Yew examples. How can hot reloading be setup ? Or what is your workflow like ?

Collapse
davidedelpapa profile image
Davide Del Papa Author

Ok, if you take a look at the end of tutorial 6 there you have it: hot reloading working like a charm. I use two scripts still, one to start and another to stop. However I already figured out something better, trying to make it into tutorial 7 or 8... They are coming out too long I reckon, that is why I keep changing things up and writing much ahead of publication. It's lot of material that I try to cover as simply as I can, but I'm not that good sadly....

Collapse
davidedelpapa profile image
Davide Del Papa Author

I'm currently working on the post for hot reloading. Let's say I got a little ahead writing because I have to check my English too.
Another thing hit reloading is possible but not always good, because rust takes time to compile: you have to be careful not to save too often..
Give me sometime. Maybe I will add something of a good workflow at the beginning of next post

Collapse
asceticbear profile image
Ascetic

There is another UI library named elvisjs implemented by pure rust and wasm, you might try it out if you are interested.

Collapse
davidedelpapa profile image
Davide Del Papa Author

Thank you. I'm actually planning a comparison between various similar tools, so I will definitely try out Elvis as well!