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.