In my previous post WASM with Rust (and Leptos) we covered creating a Rust project which generate a binary for use within WASM, using Leptos and using Trunk to build and run it.
There’s more than one framework for creating WASM/WebAssembly projects in Rust, let’s look at another one, this time Yew.
We’ll be using trunk (just as the previous post) to serve but I’ll repeat the step to install here
cargo install trunk
I’m going to assume you’ve also added the target, but I’ll include here for completeness
rustup target add wasm32-unknown-unknown
Getting started
We’re going to use a template to scaffold a basic Yew application, so create yourself a folder for your project then run
cargo generate --git https://github.com/yewstack/yew-trunk-minimal-template
For mine I stuck with the defaults after naming it wasm_app. So the stable Yew version and no logging.
Before we get into the code, let’s add a Trunk.toml (in the folder with the Cargo.toml) with this configuration
[serve] address = "127.0.0.1" port = 8081
Let’s see what Yew generated. From the app folder (mine was named wasm_app) run
trunk serve --open
Straight up, Yew gives us a colourful starting point.
In the code
Let’s go through the code, so we know what we need if we’re creating a project without the template, but also to see what’s been added.
If you check out the Cargo.toml it’s filled in a lot of package info. for us, so you might wish to go tweak there, but we have a single dependency
[dependencies]
yew = { version="0.21", features=["csr"] }
The Yew template includes index.scss for our styles and Trunk automatically compiles/transpiles to the .css file of the same name within the dist.
The index.html is lovely and simple, really the only addition from a bare bones index.html is the including the SASS link which tells the compiler to compile using SASS
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Trunk Template</title>
<link data-trunk rel="sass" href="index.scss" />
</head>
<body></body>
</html>
In the src folder we have two files, main.rs and app.rs, within main.rs we have
mod app;
use app::App;
fn main() {
yew::Renderer::<App>::new().render();
}
Here we are basically telling Yew to render our App. Within the app.rs we have
use yew::prelude::*;
#[function_component(App)]
pub fn app() -> Html {
html! {
<main>
<img class="logo" src="https://yew.rs/img/logo.svg" alt="Yew logo" />
<h1>{ "Hello World!" }</h1>
<span class="subtitle">{ "from Yew with " }<i class="heart" /></span>
</main>
}
}
Similar to Leptos, we have a macro for our HTML tags etc. but it’s html! here (not view!). Also the component is marked with the function_component annotation, but otherwise it’s very recognisable what’s happening here.
use yew::prelude::*;
#[function_component(App)]
pub fn app() -> Html {
html! {
<main>
<img class="logo" src="https://yew.rs/img/logo.svg" alt="Yew logo" />
<h1>{ "Hello World!" }</h1>
<span class="subtitle">{ "from Yew with " }<i class="heart" /></span>
</main>
}
}
Let’s add some routing
Create yourself a new file named counter.rs, let’s implement the fairly standard counter.rs component – I should say the Yew web site has an example of the counter page on their Getting Started, so we’ll just take that and make a few tweaks
use yew::prelude::*;
#[function_component(Counter)]
pub fn counter() -> Html {
let counter = use_state(|| 0);
let on_add_click = {
let c = counter.clone();
move |_| { c.set(*c + 1); }
};
let on_subtract_click = {
let c = counter.clone();
move |_| { c.set(*c - 1); }
};
html! {
<div>
<button onclick={on_add_click}>{ "+1" }</button>
<p>{ *counter }</p>
<button onclick={on_subtract_click}>{ "-1" }</button>
</div>
}
}
If you’ve used React, you’ll see this is very similar to the way we might write our React component.
Ofcourse the syntax differs, but we have a use_state and event handler functions etc. The main difference is the way we’re cloning the value – by convention all those c variables would be named counter as well, but I wanted to make it clear as to what the scope of the counter variable was.
On further reading – it appears the use_XXX syntax are hooks, see Pre-defined Hooks
When we clone the counter, we’re not cloning the value, we’re cloning the handle (or type UseStateHandler which implements Clone). All clones point to the same reactive cell, so you are essentially changing the value in that handle.
Before trying this code out we need our router, so the Yew site says add the following dependency to the Cargo.toml file
yew-router = { git = "https://github.com/yewstack/yew.git" }
but I had version issues so instead used
yew-router = { version = "0.18.0" }
Now let’s change the app.rs file to the following
use yew::prelude::*;
use yew_router::prelude::*;
use crate::counter::Counter;
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/")]
Home,
#[at("/counter")]
Counter,
#[not_found]
#[at("/404")]
NotFound,
}
fn switch(routes: Route) -> Html {
match routes {
Route::Home => html! { <h1>{ "Home" }</h1> },
Route::Counter => { html! { <Counter /> }},
Route::NotFound => html! { <h1>{ "404" }</h1> },
}
}
#[function_component(App)]
pub fn app() -> Html {
html! {
<BrowserRouter>
<Switch<Route> render={switch} />
</BrowserRouter>
}
}
There’s a fair bit to digest, but hopefully it’s fairly obvious what’s happening thankfully.
We create an enum of the routes with the at annotation mapping to the URL path. Then we use a function (named switch in this case) which maps the enum to the HTML. We’ve embedded HTML into the Home and NotFound routes but the Counter will render our Counter component as if it’s HTML.
The final change is the app functions where we use the BrowserRouter and Switch along with our switch function to render the pages.
Code
Checkout the code on GitHub