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
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
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
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
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:
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"
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
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>();
}
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>
Next, we can test our application by starting the development server by running the command below in our terminal:
trunk serve --open
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.
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>();
}
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>
}
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 ausers
property; an array type ofUser
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",
......
},
{...},
{...}
],
}
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;
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>
}
}
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>
}
}
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>
}
}
The snippet above does the following:
- Imports the required dependency
- Creates a
MessageProp
struct withtext
andcss_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>
}
}
The snippet above does the following:
- Imports yew dependency and the
User
model we created earlier - Creates a
CardProps
component props with auser
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;
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>();
}
The snippet above does the following:
- Imports the required dependencies
-
Line 13 - 14: Creates a
users
anderror
application state by using theuse_state
hook (similar tousestate
hook in React) and specifyingNone
as the initial value. TheUseStateHandle
struct is used to specify the state type and theOption
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 theuseEffect
in React to perform a side effect of fetching data from the DummyJSON API asynchronously with thewasm_bindgen_futures
andgloo_net
'sRequest::get
function. We also use thematch
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 thematch
control flow to match patterns by doing the following:- Maps through the list of
users
and pass the individualuser
to theCard
component when the API returns appropriate data - Uses the
Message
andLoader
component to match error and loading state, respectively
- Maps through the list of
-
Line 68 - 73: Updates the markup with the
Header
component anduser_list_logic
abstraction
With that done, we can restart a development server using the command below:
trunk serve --open
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: