Exploring Yew, the rust-based frontend framework as a React Developer

Demola Malomo - Sep 27 '22 - - Dev Community

WebAssembly, popularly known as WASM, has revolutionized how web applications are built. It has allowed developers to use their favourite programming languages to build web applications.

With these possibilities, developers are not tasked with the burden of learning a JavaScript-based framework when building a frontend application. They can leverage their favourite programming language features like static typing, pattern matching, memory safety, e.t.c, to build frontend applications.

Yew is a modern Rust-based framework for building frontend applications using WebAssembly.

In this post, we will learn how to build a web application using open API data from DummyJSON and Yew as a React developer.
GitHub repository can be found here.

Similarities between React and Yew

The following are the similarities between the two technologies:

Features and Functionality React Yew
Component-based structure YES YES
Minimizing DOM API call to improve performance YES YES
JavaScript support YES YES
Collection of reusable packages via NPM YES YES (Yew can leverage existing NPM packages)
Hot reloading support (seeing changes in real-time without restart) during development YES YES

Prerequisites

To fully grasp the concepts presented in this tutorial, the following requirements apply:

  • Basic understanding of React
  • Basic understanding of Rust
  • Rust installation

Development Environment Setup

First, we need to ensure the latest version of Rust is installed on our machine. We can upgrade to the stable version by running the command below:



rustup update


Enter fullscreen mode Exit fullscreen mode

Next, we need to install a WASM target, a tool that helps us compile our Rust source code into browser-based WebAssembly and makes it run on a web browser. We can install it by running the command below:



rustup target add wasm32-unknown-unknown


Enter fullscreen mode Exit fullscreen mode

Finally, we need to install Trunk, a tool for managing and packaging WebAssembly applications. We can do this by running the command below:



cargo install trunk


Enter fullscreen mode Exit fullscreen mode

Getting Started

To get started, we need to navigate to the desired directory and run the command below in our terminal:



cargo new yew-starter && cd yew-starter


Enter fullscreen mode Exit fullscreen mode

This command creates a Rust project called yew-starter and navigates into the project directory.

cargo is Rust’s package manager. It works similarly to npm in the React ecosystem.

On running the command, cargo will generate a project directory with basic files as shown below:

project directory

main.rs is the entry point of our application.

Cargo.toml is the manifest file for specifying project metadata like packages, version, e.t.c. It works similarly to package.json in a React application.

Next, we proceed to install the required dependencies by modifying the [dependencies] section of the Cargo.toml file as shown below:




//other code section goes here

[dependencies]
yew = "0.19"
serde = "1.0.144"
gloo-net = "0.2"
wasm-bindgen-futures = "0.4"


Enter fullscreen mode Exit fullscreen mode

yew = "0.19" is a Rust-based frontend framework

serde = "1.0.136" is a framework for serializing and deserializing Rust data structures. E.g. convert Rust structs to a JSON.

gloo-net= "0.2" is a HTTP requests library. It works similarly to axios in React ecosystem.

wasm-bindgen-futures = "0.4" is a Rust-based library for performing asynchronous programming in Yew by bridging the gap between Rust asynchronous programming (futures) and JavaScript Promises. Basically, It helps leverage Promise-based web APIs in Rust.

We need to run the command below to install the dependencies:



cargo build


Enter fullscreen mode Exit fullscreen mode

Application Entry Point

With the project dependencies installed, we need to modify the main.rs file inside the src folder as shown below:



use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    html! {
        <h1 class="text-primary">{ "Yew for React developers" }</h1>
    }
}

fn main() {
    yew::start_app::<App>();
}


Enter fullscreen mode Exit fullscreen mode

Oops! It looks like a lot is going on in the snippet above. Let's break it down a bit.

use yew::prelude::* : Imports the required yew dependency and its associates by specifying *

#[function_component(App)]: Declares the app function as a Functional component with App as the name. The syntax used here is called Rust macros; macros are code that writes other codes.

fn app() -> Html {…..} : Uses the html! macro to create Yew for React developers markup. The macro works similarly to JSX in React.

yew::start_app::<App>(): Starts a Yew application by mounting the App component to the body of the document. It works similarly to ReactDOM.render function in React.

HTML Render
Similarly to the way React renders into the DOM, the same principles apply in Yew. Next, we need to create an index.html file with Bootstrap CDN support in the root directory of our project and add the snippet below:



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
    <title>Yew Starter</title>
</head>
<body>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

index file

Next, we can test our application by starting the development server by running the command below in our terminal:



trunk serve --open


Enter fullscreen mode Exit fullscreen mode

Running application

Building a real application with Yew

Now that we have a good grasp of how Yew works, we can proceed to build an application that integrates DummyJSON’s user API.

Module system in Rust
In React, components form the building block of an application. In our application, we will use Rust Module system to structure our application.

To do this, we need to navigate to the src folder and create the component and model folder with their corresponding mod.rs file to manage visibility.

Modules

To use the code in the modules, we need to declare them as a module and import them into the main.rs file as shown below:



use yew::prelude::*;

//add below
mod components;
mod models;

#[function_component(App)]
fn app() -> Html {
  //app code goes here
}

fn main() {
    yew::start_app::<App>();
}


Enter fullscreen mode Exit fullscreen mode

Creating Models
With that done, we need to create models to represent the response returned from the API. To do this, we need to navigate to the models folder, here, create a user.rs file and add the snippet below:



use serde::Deserialize;

#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct User {
    pub first_name: String,
    pub last_name: String,
    pub email: String,
    pub gender: String,
    pub phone: String,
}

#[derive(Clone, Deserialize, PartialEq)]
pub struct Users {
    pub users: Vec<User>
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Uses the derive macro to generate implementation support for formatting the output and deserializing the data structure. The #[serde(rename_all = "camelCase")] macro converts snake case properties to camel case (The API returns data in camel case)
  • Creates a User struct with required properties needed from the API nested response
  • Creates a Users struct with a users property; an array type of User struct. Dynamic arrays in Rust are represented as a vector

Sample of API response from DummyJSON below:



{
  "users": [
    {
      "id": 1,
      "firstName": "Terry",
      "lastName": "Medhurst",
      "maidenName": "Smitham",
      "age": 50,
      "gender": "male",
      "email": "atuny0@sohu.com",
      ......
    },
      {...},
      {...}
    ],
}


Enter fullscreen mode Exit fullscreen mode

PS: The pub modifier makes the struct and its property public and can be accessed from other files/modules.

Next, we must register the user.rs file as part of the models module. To do this, open the mod.rs in the models folder and add the snippet below:



pub mod user;


Enter fullscreen mode Exit fullscreen mode

Creating Components
With the models fully setup, we can start creating our application building blocks.

First, we need to navigate to the components folder and create a header.rs file and add the snippet below:



use yew::prelude::*;

#[function_component(Header)]
pub fn header() -> Html {
    html! {
    <nav class="navbar bg-black">
        <div class="container-fluid">
            <a class="navbar-brand text-white" href="#">{"User List"}</a>
        </div>
    </nav>
    }
}


Enter fullscreen mode Exit fullscreen mode

The snippet above creates a Header component to represent our application header.

Secondly, we need to create a loader.rs file in the same components folder and add the snippet below:



use yew::prelude::*;

#[function_component(Loader)]
pub fn loader() -> Html {
    html! {
    <div class="spinner-border" role="status">
        <span class="visually-hidden">{"Loading..."}</span>
    </div>
    }
}


Enter fullscreen mode Exit fullscreen mode

The snippet above creates a Loader component representing a UI when our application is loading.

Thirdly, we need to create a message.rs file in the same components folders and add the snippet below:



use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct MessageProp {
    pub text: String,
    pub css_class: String,
}

#[function_component(Message)]
pub fn message(MessageProp { text, css_class }: &MessageProp) -> Html {
    html! {
        <p class={css_class.clone()}>
            {text.clone()}
        </p>
    }
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Creates a MessageProp struct with text and css_class properties to represent the component property. The #[derive(Properties, PartialEq)] macros marks the struct as a component prop similar to a React application
  • Destructures the props and use them as CSS class and display text in the markup

Fourthly, we need to create a card.rs file in the same components folders and add the snippet below:



use yew::prelude::*;
use crate::models::user::User;

#[derive(Properties, PartialEq)]
pub struct CardProp {
    pub user: User,
}

#[function_component(Card)]
pub fn card(CardProp { user }: &CardProp) -> Html {
    html! {
    <div class="m-3 p-4 border rounded d-flex align-items-center">
        <img src="https://robohash.org/hicveldicta.png?size=50x50&set=set1" class="mr-2" alt="img" />
        <div class="">
            <p class="fw-bold mb-1">{format!("{} {}", user.first_name.clone(), user.last_name.clone())}</p>
            <p class="fw-normal mb-1">{user.gender.clone()}</p>
            <p class="fw-normal mb-1">{user.email.clone()}</p>
            <p class="fw-normal mb-1">{user.phone.clone()}</p>
        </div>
    </div>
    }
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports yew dependency and the User model we created earlier
  • Creates a CardProps component props with a user property
  • Destructures the props to display the user information in the UI

Finally, we must register the newly created components as part of the components module. To do this, open the mod.rs in the components folder and add the snippet below:



pub mod header;
pub mod loader;
pub mod card;
pub mod message;


Enter fullscreen mode Exit fullscreen mode

Putting it all together
With the application components created, we can start using them to build our application by modifying the main.rs file as shown below:



use yew::prelude::*;

mod components;
mod models;

use gloo_net::{http::Request, Error}; //add
use models::user::Users; //add

use components::{card::Card, header::Header, loader::Loader, message::Message}; //add

#[function_component(App)]
fn app() -> Html {
    let users: UseStateHandle<Option<Users>> = use_state(|| None);
    let error: UseStateHandle<Option<Error>> = use_state(|| None);

    {
        //create copies of states
        let users = users.clone();
        let error = error.clone();

        use_effect_with_deps(
            move |_| {
                wasm_bindgen_futures::spawn_local(async move {
                    let fetched_users = Request::get("https://dummyjson.com/users").send().await;
                    match fetched_users {
                        Ok(response) => {
                            let json = response.json::<Users>().await;
                            match json {
                                Ok(json_resp) => {
                                    users.set(Some(json_resp));
                                }
                                Err(e) => error.set(Some(e)),
                            }
                        }
                        Err(e) => error.set(Some(e)),
                    }
                });
                || ()
            },
            (),
        );
    }

    let user_list_logic = match users.as_ref() {
        Some(users) => users
            .users
            .iter()
            .map(|user| {
                html! {
                  <Card user={user.clone() }/>
                }
            })
            .collect(),
        None => match error.as_ref() {
            Some(_) => {
                html! {
                    <Message text={"Error getting list of users"} css_class={"text-danger"}/>
                }
            }
            None => {
                html! {
                  <Loader />
                }
            }
        },
    };

    html! {
      <>
        <Header />
        {user_list_logic}
      </>
    }
}

fn main() {
    yew::start_app::<App>();
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Line 13 - 14: Creates a users and error application state by using the use_state hook (similar to usestate hook in React) and specifying None as the initial value. The UseStateHandle struct is used to specify the state type and the Option enum represents an optional value
  • Line 18 - 19: Creates a copy of the states for safe use within the current scope
  • Line 21 - 42: Uses the use_effect_with_deps hook similar to the useEffect in React to perform a side effect of fetching data from the DummyJSON API asynchronously with the wasm_bindgen_futures and gloo_net's Request::get function. We also use the match control flow to match JSON response returned by updating the states accordingly
  • Line 44 - 66: Creates a user_list_logic variable to abstract our application logic by using the match control flow to match patterns by doing the following:
    • Maps through the list of users and pass the individual user to the Card component when the API returns appropriate data
    • Uses the Message and Loader component to match error and loading state, respectively
  • Line 68 - 73: Updates the markup with the Header component and user_list_logic abstraction

With that done, we can restart a development server using the command below:



trunk serve --open


Enter fullscreen mode Exit fullscreen mode

https://media.giphy.com/media/i9ADzKhTZh5zV0I8LB/giphy.gif

Conclusion

This post discussed how to create a web application using open API data from DummyJSON and Yew.

These resources might be helpful:

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