Intro to Yew, a Rust Frontend Framework

K - Nov 16 '20 - - Dev Community

Everyone is talking about Rust lately, and it seems to be delivering what other C/C++ killers didn't. Safe, high-performance system software.

But with the dawn of WebAssembly (WASM), Rust can also dive into the frontend realm. Since Rust has a minimal runtime, it's one of the few programming languages that can actually deliver sane package sizes for WASM based web frontends.

The creators of the Yew framework try to tackle that problem. Is Yew smaller than JavaScript-based frameworks? No! Is it faster? Also No! But it's easier? Well... No.

So why am I looking into this?!

Well, I guess in the hope that a Yew developer chimes in and tells me that I did it wrong! :D

Also, because I want to learn a bit more Rust and know mostly stuff about frontend development, so I can at least re-use some of my skills while getting the hang of it.

I talked so much about backend stuff on "Fullstack Frontend", I thought it was time to write some frontend content again.

The Bootstrap Starter

In this article, I will try to build the Bootstrap starter template with Yew. So no interactions, just setting the development environment up and pump some HTML into it.

Prerequisites

I assume you have Rust and Cargo installed.

Setting Up the Environment

Install wasm-pack, a WebAssembly build-tool for Rust.

$ cargo install wasm-pack
Enter fullscreen mode Exit fullscreen mode

Creating the Rust Project

Use Cargo to create a new project.

$ cargo new --lib bootstrap-starter
$ cd bootstrap-starter
Enter fullscreen mode Exit fullscreen mode

Configuring the Packages

Cargo automatically downloads packages you add as dependencies to the Cargo.toml.

[package]
name = "bootstrap-starter"
version = "0.1.0"
authors = ["John Doe <dev@example.com>"]
edition = "2018"

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

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

We need two dependencies here. wasm-bindgen is used to link up the WASM code with JavaScript and, well, the Yew framework itself.

Creating the HTML File

WASM apps can't stand alone in the browser; they have to be loaded via JavaScript, which in turn has to be embedded into HTML.

So let's create a static HTML file static/index.html with this content:

<!doctype html>

<title>Bootstrap Starter</title>

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">

<style type="text/css">
body { padding-top: 5rem }
.starter-template {
  padding: 3rem 1.5rem;
  text-align: center;
}
</style>

<script type="module">
  import init from "./wasm.js";
  init();
</script>
Enter fullscreen mode Exit fullscreen mode

WASM-Pack will create a static/wasm.js file that our JavaScript can load later. I also added some CSS and Bootstrap to make the example look at least a bit interesting.

Creating the Components

I used Bootstrap in this example, the starter template, be exact.

I tried to split it into two components, nothing fancy. No interaction, just minimal Yew components.

Let's replace the content of your src/lib.rs file with the following content:

#![recursion_limit="1000"]

mod content;
mod navigation;

use content::Content;
use navigation::Navigation;
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! {
            <div>
                <Navigation />
                <Content />
            </div>
        }
    }
}

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

This is what came up as I tried to remove as much code from the Yew examples as possible while still getting it to compile.

Rust doesn't have classes, and functional components are still in the making, so a struct with a trait implementation is as close as Yew currently gets to React class components.

If you come from a React background, you'll notice the view method that uses an html! macro, allowing JSX like syntax. The other methods are create, which is the constructor of the component. update, which handles interactions via messages as Redux would, I guess? And finally, change which handles new props that come from the parent component. They all don't return anything interesting because I didn't implement any interactions, but the Component trait doesn't come with default implementations for them, so I had to do it myself.

The first line tells the compiler not to blow up when the html! macro's HTML is nested too deep.

The mod statements import the other components, which we will create next. The use statements make some things a bit easier to... well... use, haha. After use content::Content; we can use Content inside the html! macro like it wasn't defined in another file.

The use statements for wasm_bindgen and yew make some other code available to run the app at the end of the file. A bit like the render function of ReactDOM, just with the extra step of calling out of WASM into JavaScript.

For the Content component, you have to create a new file src/content.rs with the following code:

use yew::prelude::*;

pub struct Content {}

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

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

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

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

    fn view(&self) -> Html {
        html! {
            <main class="container">
                <div class="starter-template">
                    <h1>{"Bootstrap starter template"}</h1>
                    <p class="lead">
                        {"Use this document as a way to quickly start any new project."}
                        <br/>
                        {"All you get is this text and a mostly barebones HTML document."}
                    </p>
                </div>
            </main>
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Nothing too spectacular here. As with JSX, you need to close your tags properly. But different from JSX, you have to wrap your strings into curly braces and quotation marks.

The navigation component isn't much different. Create a new file src/navigation.rs and fill this code in:

use yew::prelude::*;

pub struct Navigation {}

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

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

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

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

    fn view(&self) -> Html {
        html! {
            <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
                <a class="navbar-brand" href="#">{"Navbar"}</a>
                <button class="navbar-toggler">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarsExampleDefault">
                    <ul class="navbar-nav mr-auto">
                        <li class="nav-item active">
                            <a class="nav-link" href="#">
                                {"Home "}<span class="sr-only">{"(current)"}</span>
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="https://example.com">{"Link"}</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link disabled" href="#">{"Disabled"}</a>
                        </li>
                    </ul>
                    <form class="form-inline my-2 my-lg-0">
                        <input class="form-control mr-sm-2" type="text" placeholder="Search" />
                        <button class="btn btn-outline-success my-2 my-sm-0" type="submit">{"Search"}</button>
                    </form>
                </div>
            </nav>
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Again, nothing special here. The input tag has to be closed properly, and the text-nodes have to be inside curly braces and quotation-marks.

Build the App

If you run the following command, you should end up with more files inside the static directory besides the index.html.

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

These files contain the actual Rust code compiled to WASM and many wasm-pack/wasm-bindgen helpers to link up the WASM with JavaScript.

I ended up with about 140KB, which is quite steep compared to JavaScript, but I didn't optimize anything here, just plain out of the box compilation.

On the other hand, it's still much smaller than what other languages that compile to WASM currently achieve, especially those with giant runtimes.

Running the Example

If you point an HTTP server to the static directory, you will see nothing out of the ordinary. But if you look into the dev-tools' network tab, you will see what files are actually loaded and how big they are.

I used the miniserve crate that was proposed on the Yew guide. It's installed with:

$ cargo +nightly install miniserve
Enter fullscreen mode Exit fullscreen mode

But it could be that you have to install the nightly toolchain before.

$ rustup toolchain install nightly 
Enter fullscreen mode Exit fullscreen mode

After that, you can run miniserve and look at your fresh WASM frontend:

$ miniserve ./static --index index.html
Enter fullscreen mode Exit fullscreen mode

Summary

Yew is an interesting approach to front-end development. Rust's type-system is really nice and can prevent many JavaScript-related errors.

Also, the html! macro is a nice touch and certainly better integrated into Rust than JSX is into JavaScript (because it isn't really).

I didn't build anything complex here, so I can't say anything about the rest of the framework, just that the size isn't perfect, and the benchmarks I saw didn't look very good either. But I guess this will change when WASM gets better integration with the browser in the future.

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