Back to Articles

29/09/2022 13:03

YEW WASM Rust Framework

green shimmerhing lights in the sky digital art by
Article Cover image

WASM with RUST from a REACT perspective

The Catch!

This presentation is supposed to give you an easy entry into the WASM world. So you’ve been hearing about this WASM and Rust lately a lot? Well, this is the read to pull you into the rabbit hole! If you’re into performance focused next gen technology, keep watching/reading!

Why should I care?

RUST has been steadily growing with its ecosystem. RUST treats WASM as a first class citizen, and it already spawned quite a few Reactive libraries like: “Sycamore”, “Yew” and “Dioxus” most of them already outperforming REACT and alike by a far. The job market for RUST is also steadily climbing with a lot of new interesting projects and opportunity’s. The crypto world mostly revolves around RUST, so this is a good stepping stone from going from JavaScript to RUST for new learning opportunities! There are more reasons about the language itself as to why it might be interesting to you! But to keep this article on topic we’ll concentrate on WebAssembly.

The Beginning

YEW is a Rust WASM Framework to create Reactive Web Applications what YEW advertises with its web worker integration and fearless concurrency YEW copies the REACT API style, so if you know the REACT API you should have no problem

Sneakpeek YEW and RUST Syntax

counter increment example

use yew::prelude::*;

#[function_component(App)]
pub fn app() -> Html {
    let counter = use_state(|| 0);
    let onclick = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1))
    };

    html! {
        <main>
            <p>{*counter}</p>
            <button {onclick}>{"Click Me!"}</button>
        </main>
    }
}

compared to JS

import React, { useState } from 'react';

const Example = () => {
  const [counter, setCount] = useState(0);
  const onClick = () => {
    setCount(counter + 1)
  }

  return (
    <main>
      <p>{counter}</p>
      <button onClick={onClick}>
        Click me
      </button>
    </main>
  );
}

The Setup

To give us a full local rust and wasm env, we’ll need

  • brew install rustup
  • rustup update
  • rustup install nightly
  • rustup target add wasm32-unknown-unknown
  • cargo install trunk

Lets build

What do we need nowadays for most modern applications ?

  • Reactivity / state / global state
  • Events
  • Routing
  • data fetching / API

Reactivity

/// reactivity
let counter = use_state(|| 0);

dereferencing the counter *counter gives us the value and calling counter.set() allows us to write to the state

Context

use yew::prelude::*;

#[derive(Clone, Debug, PartialEq)]
struct Theme {
    foreground: String,
    background: String,
}

#[function_component(App)]
pub fn app() -> Html {
    let counter = use_state(|| 0);
    let onclick = {
        let counter = counter.clone();
        Callback::from(move |_| counter.set(*counter + 1))
    };
    let ctx = use_state(|| Theme {
        foreground: "#000000".to_owned(),
        background: "#eeeeee".to_owned(),
    });

    html! {
        <ContextProvider<UseStateHandle<Theme>> context={ctx.clone()}>
            <main>
                <p style={format!("color: {}", (*ctx).foreground)}>{*counter}</p>
                <button {onclick}>{"Click Me!"}</button>
                <ChangeColor />
            </main>
        </ContextProvider<UseStateHandle<Theme>>>
    }
}

#[function_component(ChangeColor)]
pub fn change_color() -> Html {
    let theme = use_context::<UseStateHandle<Theme>>().expect("Not Ctx found");
    let onclick = {
        let theme = theme.clone();
        Callback::from(move |_| {
            theme.set(Theme {
                foreground: "#3dff3d".to_owned(),
                background: theme.background.clone(),
            });
        })
    };

    html! {
        <div>
            <button {onclick}>{format!("Current Color {}", (*theme).foreground)}</button>
        </div>
    }
}

Events

Obviously, we need to be able to interact with our application

const Example = () => {
  const [counter, setCount] = useState(0);
  const onClick = () => {
    setCount(counter + 1)
  }

  return (
    <main>
      <p>{counter}</p>
      <button onClick={onClick}>
        Click me
      </button>
    </main>
  );
}

Routing

Most likely we won’t be only doing one page application in which case we also want to implement routing

#[derive(clone, routable, partialeq)]
enum route {
    #[at("/")]
    home,
    #[at("/secure")]
    secure,
    #[at("/movies")]
    movies,
    #[not_found]
    #[at("/404")]
    notfound,
}

#[function_component(secure)]
fn secure() -> html {
    html! {
        <div>
            <h1>{ "secure" }</h1>
            <button onclick={change_route(route::home)}>{ "go home" }</button>
        </div>
    }
}

fn change_route(route: route) -> yew::callback<mouseevent> {
    let history = use_history().unwrap();
    callback::once(move |_| history.push(route))
}

Data Fetching

For data fetching, we’ll use reqwasm on the client side and reqwest on the server side

#[cfg(not(target_arch = "wasm32"))]

allows us to create the same functions for different targets to streamline our functions for both targets

/// data fetching
#[cfg(not(target_arch = "wasm32"))]
pub async fn login(&self, username: String, password: String) -> Result<String, Error> {
    let url = format!("{}/login", self.base_url);
    let payload = AuthLoginPayload { username, password };
    let client = reqwest::Client::new();
    let res = client
        .post(&url)
        .json(&payload)
        .send()
        .await?
        .json::<AuthResponse>()
        .await?;
    Ok(res.token)
}

#[cfg(target_arch = "wasm32")]
pub async fn login(&self, username: String, password: String) -> Result<String, Error> {
    let url = format!("{}/login", self.base_url);
    let payload = AuthLoginPayload { username, password };
    let res = reqwasm::http::Request::post(&url)
        .body(serde_wasm_bindgen::to_value(&payload).unwrap())
        .send()
        .await
        .unwrap()
        .json::<AuthResponse>()
        .await
        .unwrap();
    Ok(res.token)
}

The Full Combined Setup

use crate::context::context::MovieContext;
use reqwasm::http::Request;
use yew::prelude::*;
use yew_router::prelude::*;

#[derive(clone, routable, partialeq)]
enum route {
    #[at("/")]
    home,
    #[at("/secure")]
    secure,
    #[at("/movies")]
    movies,
    #[not_found]
    #[at("/404")]
    notfound,
}

#[function_component(secure)]
fn secure() -> html {
    html! {
        <div>
            <h1>{ "secure" }</h1>
            <button onclick={change_route(route::home)}>{ "go home" }</button>
        </div>
    }
}

fn change_route(route: route) -> yew::callback<mouseevent> {
    let history = use_history().unwrap();
    callback::once(move |_| history.push(route))
}

#[function_component(Home)]
fn home() -> Html {
    html! {
        <div>
            <h1>{ "Home" }</h1>
            <button onclick={change_route(Route::Movies)}>{ "Movies" }</button>
            <button onclick={change_route(Route::Secure)}>{ "Secure" }</button>
        </div>
    }
}

#[function_component(Movies)]
fn movies() -> Html {
    let movies = use_context::<Vec<MovieContext>>().expect("no ctx found");
    if movies.len() == 0 {
        return html! {<div></div>};
    }

    html! {
        <div>
            <h1>{ "Movies" }</h1>
            {movies.iter().map(|el| html! {<div>{el.title.clone()}</div>}).collect::<Html>()}
            <button onclick={change_route(Route::Home)}>{ "Go Home" }</button>
        </div>
    }
}

fn switch(routes: &Route) -> Html {
    match routes {
        Route::Home => html! { <Home /> },
        Route::Movies => html! { <Movies /> },
        Route::Secure => html! {
            <Secure />
        },
        Route::NotFound => html! { <h1>{ "404" }</h1> },
    }
}

#[function_component(App)]
pub fn app() -> Html {
    let movies: UseStateHandle<Vec<MovieContext>> = use_state(|| vec![]);
    {
        let movies = movies.clone();
        use_effect_with_deps(
            move |_| {
                let movies = movies.clone();
                wasm_bindgen_futures::spawn_local(async move {
                    let fetched_videos: Vec<MovieContext> =
                        Request::get("http://localhost:8080/tutorial/data.json")
                            .send()
                            .await
                            .unwrap()
                            .json()
                            .await
                            .unwrap();
                    movies.set(fetched_videos);
                });
                || ()
            },
            (),
        );
    }

    html! {
        <ContextProvider<Vec<MovieContext>> context={(*movies).clone()}>
            <BrowserRouter>
                <Switch<Route> render={Switch::render(switch)} />
            </BrowserRouter>
        </ContextProvider<Vec<MovieContext>>>
    }
}

SSR and Hydration

There are crates that allow you to achieve SSR for example “Perseus” which could be compared to NextJS in a way. So if you feel up to the task, why not try to set up a project with perseus and yew?

Pros

Fearless Concurrency if needed. State-of-the-art error handling. Faster speeds with task that involve heavy computation. Interoperability with JavaScript. You’re forced to do it the right way.

Cons

If your key audience relies on older browser version WASM might not be supported therefore is then not an option for you. WASM bundle asset size can be bigger than JS size. WASM can unfold its true power with tasks that involve a lot of computation, for instance big list rerenderings. There might be use cases where JavaScript due to the bundle sizes and small app complexity make it more appealing. Slower development speed it’s a double-edged sword because it comes also with pro’s. Initial learning curve attached to rust when going into mid to advanced territory.

Epilogue

As we can see, with REACT knowledge you can translate pretty well to YEW We’ll have some overhead in learning the rust syntax, rules borrowing lifetimes and std lib API but it’s manageable and comes with a lot of advantages like error handling and a proper compiler that wants you to do the stuff in the right way.

Sources

The masccoot of the website mr raccoon!