In this article, you will learn how to build a modern, single-page frontend application in Rust using the Yew.rs framework and WebAssembly. Our app will include essential JWT authentication features such as user registration, login, logout, restrict access to protected pages, and the ability to refresh access tokens in the background when they expire.

If you have prior experience with React.js or any framework that uses the virtual DOM architecture, you’ll find working with Yew.rs to be relatively straightforward. Yew.rs provides similar tools for building user interfaces, including components, events, and state management.

While there are several CSS-in-Rust styling solutions and styling component libraries available, we’ll be using Tailwind due to its ease of use and simplicity. With Tailwind, we can easily style our components without writing custom CSS.

So, without further ado, let’s get started and learn how to build a modern, robust frontend application in Rust using Yew.rs!

More practice:

Rust and Yew.rs Frontend JWT Access and Refresh Tokens

Run the Yew.rs Frontend App Locally

  1. Ensure that you have Trunk installed and the WebAssembly target set up. You can install the target by visiting https://yew.rs/docs/getting-started/introduction.
  2. Download or clone the RS256 Rust Yew.rs project from https://github.com/wpcodevo/rust-yew-rs256-web-app and open it in your preferred code editor.
  3. Open the integrated terminal and run the command trunk build in your IDE or text editor. This installs the necessary crates and creates asset-build pipelines for the assets specified in the index.html file.
  4. Once the installation and asset build are complete, start the Yew web server by running trunk serve --port 3000 in the terminal. This creates a local web server on your machine that you can access via your web browser at localhost:3000.
  5. Interact with the app to register, log in, and log out users, request your account details, and refresh the access token in the background when it expires.

Run the Frontend App with a Backend API

If you need a comprehensive guide on how to implement the RS256 JWT authentication in Rust, check out the post “Rust and Actix Web – JWT Access and Refresh Tokens“. Otherwise, you can follow the instructions below to quickly set up the API and use the Yew.rs application to interact with it.

  1. Download or clone the Rust JWT RS256 project from https://github.com/wpcodevo/rust-jwt-rs256 and open it in an IDE or text editor.
  2. Launch the PostgreSQL, Redis, and pgAdmin servers by running docker-compose up -d in the terminal of the root directory.
  3. Install the SQLx CLI by running cargo install sqlx-cli if you haven’t already. Then push the migration script to the PostgreSQL database with sqlx migrate run.
  4. Install the required crates and build the project with cargo build.
  5. Start the Actix-Web HTTP server with cargo r.
  6. To test the RS256 JWT authentication, import the Rust_JWT_RS256.postman_collection.json file into Postman or Thunder Client VS Code extension, and test each endpoint to confirm that they work correctly. Alternatively, you can use the Yew.rs app to interact with the API.

Bootstrap the Yew.rs Project

Upon finishing this tutorial, your file and folder organization will look similar to the one in the screenshot below.

Folder Structure of the Rust Yew.rs JWT RS256 Project

To kick things off, choose a location on your computer where you want to store the project’s source code. Once you’ve picked a spot, open a terminal in that directory and enter the following commands. These commands will create a new folder named rust-yew-rs256-web-app, navigate to that folder, and use Cargo to initialize the Rust project.


mkdir rust-yew-rs256-web-app
cd rust-yew-rs256-web-app
cargo init

After initializing the Rust project, open it in your favourite text editor or IDE. For this tutorial, I will be using VS Code. Once the project is open, go to the integrated terminal in your IDE and run the following commands to install the required crates for the project.


cargo add yew --features csr
cargo add serde_json
cargo add serde --features derive
cargo add chrono --features serde
cargo add reqwasm
cargo add yew-router
cargo add gloo
cargo add validator --features derive
cargo add yewdux
cargo add wasm-bindgen
cargo add web-sys --features "HtmlInputElement Window"
cargo add wasm-bindgen-futures

  • yew – Yew is a Rust framework that utilizes WebAssembly and a virtual DOM to provide fast rendering and reactive components that update automatically when data changes.
  • serde_json – For serializing and deserializing JSON data in Rust.
  • serde – Provides a simple and efficient way to convert Rust data structures into a format that can be stored or transmitted, and vice versa.
  • chrono – Date and time library for Rust
  • reqwasm – A Rust crate that provides a set of APIs for making HTTP requests from WebAssembly applications running in the browser. It is built on top of the web-sys and wasm-bindgen crates, which provide low-level access to the Web APIs and the JavaScript runtime environment.
  • yew-router – Is a routing library for the Yew web framework in Rust. It provides a simple and flexible way to handle client-side routing in single-page applications.
  • gloo – Provides a low-level API for working with the browser’s Web API, such as the DOM and fetch, and also includes abstractions for managing events, timers, and other common tasks.
  • validator – For validating and sanitizing input data, such as user input in a web application.
  • yewdux – Provides a predictable state container for Yew applications, inspired by Redux. It allows developers to manage the state of their application in a single location, making it easier to reason about and debug.
  • wasm-bindgen – This crate provides bindings between Rust and JavaScript, enabling Rust code to be called from JavaScript and vice versa.
  • web-sys – This crate provides Rust bindings to the APIs exposed by web browsers such as DOM manipulation, WebGL, WebSockets, and many others.
  • wasm-bindgen-futures – This crate provides a bridge between JavaScript Promises and Rust Futures, allowing asynchronous operations to be composed and executed in a Rust-y way.

Before we proceed, it’s worth mentioning that Rust and its crates are continually evolving, which may result in breaking changes that could cause errors in your application. In such cases, you can use the versions specified in the Cargo.toml file provided below. If you encounter any issues with a particular crate, please leave a comment so that I can address them accordingly.

Cargo.toml


[package]
name = "rust-yew-rs256-web-app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
gloo = "0.8.0"
reqwasm = "0.5.0"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
validator = { version = "0.16.0", features = ["derive"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.34"
web-sys = { version = "0.3.61", features = ["HtmlInputElement","Window"] }
yew = { version = "0.20.0", features = ["csr"] }
yew-router = "0.17.0"
yewdux = "0.9.0"

Now, it’s time to start writing some code. We’ll begin by creating a simple Yew component that displays some text on the screen. To do this, open the src/main.rs file and replace its current contents with the following code.

src/main.rs


use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    html! {
        <h1>{"Rust and Yew.rs Frontend App: RS256 JWT Access and Refresh Tokens"}</h1>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

To run the Yew application, we’ll use a tool called Trunk, which is similar to Vite or Webpack in JavaScript. Trunk can bundle the application and ship it to the browser as a WebAssembly application. If you don’t have Trunk installed, you can follow the instructions at https://yew.rs/docs/getting-started/introduction to install it and the WebAssembly target.

Trunk needs a source HTML file in the root directory to drive all asset building and bundling. So, let’s create an index.html file in the project’s root directory and generate a boilerplate HTML code with Emmet. Below is an example of the code you can use.

index.html


<!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" />
    <title>My First Yew App</title>
  </head>
  <body></body>
</html>

After completing the previous steps, you can run the command trunk serve --port 3000 to install the required crates, build the project, and start a local development server on port 3000. Once the server is up and running, simply head over to http://localhost:3000 to view the H1 text displayed on the screen.

Setup the Yew.rs application to Display an H1 text

Setup Tailwind CSS

Now let’s set up Tailwind CSS to help us style our Yew components. Since this is a Rust project, we can use NPX to avoid creating a package.json file and installing packages in a node_modules folder. To generate the Tailwind CSS configuration files, simply run the following command:


npx tailwindcss init -p

To make Tailwind CSS work with our Rust project, we need to configure it to recognize all of our template files and add custom colours and fonts. Open the tailwind.config.js file and replace its contents with the following code:

tailwind.config.js


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{rs,html}"
  ],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
        'ct-red-500': '#ef4444',
      },
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};


Configuring Tailwind CSS is only the first step in using it. The next step is to create a CSS file that includes the Tailwind CSS directives. To do this, create a folder named “styles” in the project’s root directory if it doesn’t already exist. Then, create a file named tailwind.css inside the “styles” directory and add the following CSS code to it.

styles/tailwind.css


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

html {
    font-family: 'Poppins', sans-serif;
}

After using Tailwind CSS utility classes in a project, it’s necessary to run the Tailwind CSS command to compile them and output the final CSS into a file. However, instead of manually running this command, we can use Trunk’s post-build hook to automatically execute it for us whenever changes are made to the source code.

To set this up, create a Trunk.toml file in the root directory and add the following configurations.

Trunk.toml


[[hooks]]
stage = "post_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i ./styles/tailwind.css -o ./dist/.stage/index.css"]

To finalize the setup, we need to link the index.css file generated by Tailwind CSS in the HTML link tag to make the CSS code available to the application. Additionally, we need to specify a logo.svg file that will serve as the application’s favicon. To use the Yew.rs logo, you can download it from the official website and save it in the root directory of your project.

index.html


<!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 data-trunk rel="icon" href="logo.svg" type="image/svg" />
    <link rel="stylesheet" href="./index.css" />
    <title>My First Yew App</title>
  </head>
  <body></body>
</html>

Define the API Request Functions

Let’s create some handy functions that can be used to make requests to the backend API. Instead of adding this logic directly to the Yew components, we’ll create a separate module for it. This way, our components will look simpler and cleaner. Since we’ll be shipping the project as a WebAssembly project, we’ll use a library called Reqwasm to handle the HTTP requests.

API Response Types

Before creating the helper functions to handle the API requests, we need to define the structure of the data that we will be receiving from the backend server. This involves creating structs that will be used to type the responses.

To do this, create a new folder called ‘api‘ inside the ‘src‘ directory. Within the api folder, create a file called types.rs and add the following structs inside it.

src/api/types.rs


use chrono::prelude::*;
use serde::{Deserialize, Serialize};

#[allow(non_snake_case)]
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
pub struct User {
    pub id: String,
    pub name: String,
    pub email: String,
    pub role: String,
    pub photo: String,
    pub verified: bool,
    pub createdAt: DateTime<Utc>,
    pub updatedAt: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserData {
    pub user: User,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserResponse {
    pub status: String,
    pub data: UserData,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserLoginResponse {
    pub status: String,
    pub access_token: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ErrorResponse {
    pub status: String,
    pub message: String,
}

Create the API Requests

Now it’s time to create the API request functions that will allow us to communicate with the backend server. To get started, go to the api directory and create a new file named user_api.rs. Then, copy and paste the following code into the file.

src/api/user_api.rs


use super::types::{ErrorResponse, User, UserLoginResponse, UserResponse};
use reqwasm::http;

pub async fn api_register_user(user_data: &str) -> Result<User, String> {
    let response = match http::Request::post("http://localhost:8000/api/auth/register")
        .header("Content-Type", "application/json")
        .body(user_data)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserResponse>().await;
    match res_json {
        Ok(data) => Ok(data.data.user),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_login_user(credentials: &str) -> Result<UserLoginResponse, String> {
    let response = match http::Request::post("http://localhost:8000/api/auth/login")
        .header("Content-Type", "application/json")
        .credentials(http::RequestCredentials::Include)
        .body(credentials)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserLoginResponse>().await;
    match res_json {
        Ok(data) => Ok(data),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_refresh_access_token() -> Result<UserLoginResponse, String> {
    let response = match http::Request::get("http://localhost:8000/api/auth/refresh")
        .header("Content-Type", "application/json")
        .credentials(http::RequestCredentials::Include)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserLoginResponse>().await;
    match res_json {
        Ok(data) => Ok(data),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_user_info() -> Result<User, String> {
    let response = match http::Request::get("http://localhost:8000/api/users/me")
        .credentials(http::RequestCredentials::Include)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserResponse>().await;
    match res_json {
        Ok(data) => Ok(data.data.user),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_logout_user() -> Result<(), String> {
    let response = match http::Request::get("http://localhost:8000/api/auth/logout")
        .credentials(http::RequestCredentials::Include)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    Ok(())
}

  • api_register_user: This function sends a POST request with the form data included in the request body to the /api/auth/register endpoint and registers a new user on the backend.
  • api_login_user: This function sends a POST request with the user’s credentials included in the request body to the /api/auth/login endpoint and retrieves an access token from the backend server.
  • api_refresh_access_token: This function is responsible for retrieving new access tokens from the backend server.
  • api_user_info: This function sends a GET request to the /api/users/me endpoint to retrieve the authenticated user’s information.
  • api_logout_user: This function is responsible for logging out the currently logged-in user.

To make these functions available to other modules or files in the application, create a mod.rs file inside the api folder and add the following code:

src/api/mod.rs


pub mod types;
pub mod user_api;

State Management with the Yewdux Library

Now, we’ll set up a global store using the Yewdux library to efficiently manage the application’s states from a central location. Although Yew provides a Context API to manage the state globally, it has some limitations. Hence, we’ll use Yewdux instead. To start, create a new file called store.rs in the src directory, and add the following code to the file.

src/store.rs


use serde::{Deserialize, Serialize};
use yewdux::prelude::*;

use crate::api::types::User;

#[derive(Debug, PartialEq, Serialize, Deserialize, Default, Clone)]
pub struct AlertInput {
    pub show_alert: bool,
    pub alert_message: String,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Store)]
pub struct Store {
    pub auth_user: Option<User>,
    pub page_loading: bool,
    pub alert_input: AlertInput,
}

pub fn set_page_loading(loading: bool, dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.page_loading = loading;
    })
}

pub fn set_auth_user(user: Option<User>, dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.auth_user = user;
    })
}

pub fn set_show_alert(message: String, dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.alert_input = AlertInput {
            alert_message: message,
            show_alert: true,
        };
    })
}

pub fn set_hide_alert(dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.alert_input.show_alert = false;
    })
}

As you can see in the code above, we’ve created functions that take the dispatch function as a parameter and use the dispatch.reduce_mut() method to modify the store’s state. By moving the state mutation logic from the components to a centralized location, which is the src/store.rs file, we can keep the components simple and uncluttered.

Create Reusable Yew.rs Components

To save time and improve the maintainability of our code, we’ll create reusable Yew components that can be used in multiple parts of the application. By doing so, we’ll make the code more readable and easier to debug in the long run.

Create a Spinner Component

First, we’ll create a Yew component called Spinner that will indicate when a request is in progress or being processed by the backend server. To begin, create a new folder named ‘components‘ in the ‘src‘ directory. Inside the componentsfolder, create a new file called spinner.rs and include the following code.

src/components/spinner.rs


use yew::prelude::*;

#[derive(Debug, Properties, PartialEq)]
pub struct Props {
    #[prop_or("1.25rem".to_string())]
    pub width: String,
    #[prop_or("1.25rem".to_string())]
    pub height: String,
    #[prop_or("text-gray-200".to_string())]
    pub color: String,
    #[prop_or("fill-blue-600".to_string())]
    pub bg_color: String,
}

#[function_component(Spinner)]
pub fn spinner_component(props: &Props) -> Html {
    html! {
    <svg
      role="status"
      class={format!(
        "mr-2 {} animate-spin dark:text-gray-600 {} h-5",
        props.color.clone(), props.bg_color.clone()
      )}
      style={format!("height:{};width:{}", props.width.clone(), props.height.clone())}
      viewBox="0 0 100 101"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
        fill="currentColor"
      />
      <path
        d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
        fill="currentFill"
      />
    </svg>
    }
}

Create a Loading Button Component

Now let’s move on to creating our second reusable Yew component – a button that incorporates the Spinner component. This button will hide the Spinner component by default, but when form data is being submitted to the backend API, it will display the Spinner to indicate that the server is processing the request.

To create this component, first, create a loading_button.rs file in the components directory, and then add the following code to it:

src/components/loading_button.rs


use super::spinner::Spinner;
use yew::prelude::*;

#[derive(Debug, Properties, PartialEq)]
pub struct Props {
    #[prop_or(false)]
    pub loading: bool,
    #[prop_or("bg-ct-yellow-600".to_string())]
    pub btn_color: String,
    #[prop_or("text-white".to_string())]
    pub text_color: String,
    pub children: Children,
}

#[function_component(LoadingButton)]
pub fn loading_button_component(props: &Props) -> Html {
    html! {
    <button
      type="submit"
      class={format!(
        "w-full py-3 font-semibold rounded-lg outline-none border-none flex justify-center {}",
         if props.loading {"bg-[#ccc]"} else {props.btn_color.as_str()}
      )}
    >
      if props.loading {
        <div class="flex items-center gap-3">
          <Spinner />
          <span class="text-slate-500 inline-block">{"Loading..."}</span>
        </div>
      }else{
        <span class={props.text_color.clone()}>{props.children.clone()}</span>
      }
    </button>
    }
}

Create a Notification Toast Component

To display success and error messages returned by the API, we will create a notification toast component. This component will use the setTimeout function of the browser window to automatically remove the toast component from the UI after a set amount of time.

To create the notification toast component, navigate to the ‘components‘ directory and create a new file named alert.rs. Then add the following code snippets to it.

src/components/alert.rs


use gloo::timers::callback::Timeout;
use yew::prelude::*;
use yewdux::prelude::use_store;

use crate::store::{set_hide_alert, Store};

#[derive(Debug, PartialEq, Properties)]
pub struct Props {
    pub message: String,
    pub delay_ms: u32,
}

#[function_component(AlertComponent)]
pub fn alert_component(props: &Props) -> Html {
    let (store, dispatch) = use_store::<Store>();
    let show_alert = store.alert_input.show_alert;

    use_effect_with_deps(
        move |(show_alert, dispatch, delay_ms)| {
            let cloned_dispatch = dispatch.clone();
            if *show_alert {
                let handle =
                    Timeout::new(*delay_ms, move || set_hide_alert(cloned_dispatch)).forget();
                let clear_handle = move || {
                    web_sys::Window::clear_timeout_with_handle(
                        &web_sys::window().unwrap(),
                        handle.as_f64().unwrap() as i32,
                    );
                };

                Box::new(clear_handle) as Box<dyn FnOnce()>
            } else {
                Box::new(|| {}) as Box<dyn FnOnce()>
            }
        },
        (show_alert, dispatch.clone(), props.delay_ms),
    );

    html! {
    <div id="myToast" class={format!("fixed top-14 right-10 px-5 py-4 border-r-8 border-orange-500 bg-white drop-shadow-lg {}", if show_alert { "" } else { "hidden" })}>
        <p class="text-sm">
            <span class="mr-2 inline-block px-3 py-1 rounded-full bg-blue-500 text-white font-extrabold">{"i"}</span>
            {props.message.clone()}
        </p>
    </div>
    }
}

Create a Form Input Component

This component will include an input element, a label, and a paragraph element to display error messages. Additionally, it can accept several properties, including input type, label text, input name, and callbacks for managing changes and validation errors.

To create this component, navigate to the components directory and create a new file named form_input.rs. Then, add the following code snippets to the file.

src/components/form_input.rs


use std::{cell::RefCell, rc::Rc};

use validator::ValidationErrors;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct Props {
    #[prop_or("text".to_string())]
    pub input_type: String,
    pub label: String,
    pub name: String,
    pub input_ref: NodeRef,
    pub handle_onchange: Callback<String>,
    pub handle_on_input_blur: Callback<(String, String)>,
    pub errors: Rc<RefCell<ValidationErrors>>,
}

#[function_component(FormInput)]
pub fn form_input_component(props: &Props) -> Html {
    let val_errors = props.errors.borrow();
    let errors = val_errors.field_errors().clone();
    let empty_errors = vec![];
    let error = match errors.get(&props.name.as_str()) {
        Some(error) => error,
        None => &empty_errors,
    };
    let error_message = match error.get(0) {
        Some(message) => message.to_string(),
        None => "".to_string(),
    };

    let handle_onchange = props.handle_onchange.clone();
    let onchange = Callback::from(move |event: Event| {
        let target = event.target().unwrap();
        let value = target.unchecked_into::<HtmlInputElement>().value();
        handle_onchange.emit(value);
    });

    let handle_on_input_blur = props.handle_on_input_blur.clone();
    let on_blur = {
        let cloned_input_name = props.name.clone();
        Callback::from(move |event: FocusEvent| {
            let input_name = cloned_input_name.clone();
            let target = event.target().unwrap();
            let value = target.unchecked_into::<HtmlInputElement>().value();
            handle_on_input_blur.emit((input_name, value));
        })
    };

    html! {
    <div>
      <label html={props.name.clone()} class="block text-ct-blue-600 mb-3">
        {props.label.clone()}
      </label>
      <input
        type={props.input_type.clone()}
        placeholder=""
        class="block w-full rounded-2xl appearance-none focus:outline-none py-2 px-4"
        ref={props.input_ref.clone()}
        onchange={onchange}
        onblur={on_blur}
      />
    <span class="text-red-500 text-xs pt-1 block">
        {error_message}
    </span>
    </div>
    }
}

Create a Header Component

Our last component will be a Header that includes a list of navigation links, allowing us to move between various parts of the Yew application. Although it’s designed to include logout functionality, we’ll omit it for the time being to maintain simplicity.

src/components/header.rs


use crate::{router::Route, store::Store};
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[function_component(Header)]
pub fn header_component() -> Html {
    let (store, _) = use_store::<Store>();
    let user = store.auth_user.clone();

    html! {
        <header class="bg-white h-20">
        <nav class="h-full flex justify-between container items-center">
          <div>
            <Link<Route> to={Route::HomePage} classes="text-ct-dark-600">{"CodevoWeb"}</Link<Route>>
          </div>
          <ul class="flex items-center gap-4">
            <li>
              <Link<Route> to={Route::HomePage} classes="text-ct-dark-600">{"Home"}</Link<Route>>
            </li>
            if user.is_some() {
               <>
                <li>
                  <Link<Route> to={Route::ProfilePage} classes="text-ct-dark-600">{"Profile"}</Link<Route>>
                </li>
                <li
                  class="cursor-pointer"
                >
                  {"Create Post"}
                </li>
                <li class="cursor-pointer">
                  {"Logout"}
                </li>
              </>

            } else {
              <>
                <li>
                  <Link<Route> to={Route::RegisterPage} classes="text-ct-dark-600">{"SignUp"}</Link<Route>>
                </li>
                <li>
                  <Link<Route> to={Route::LoginPage} classes="text-ct-dark-600">{"Login"}</Link<Route>>
                </li>
              </>
            }
          </ul>
        </nav>
      </header>
    }
}

Export the Components

To make the components available for use in other components, let’s create a mod.rs file in the components folder and add the following code to export them as modules.

src/components/mod.rs


pub mod alert;
pub mod form_input;
pub mod header;
pub mod loading_button;
pub mod spinner;

Implement the Authentication Flow

Now that we have finished creating the reusable components, setting up a Yewdux store, and implementing the API request functions, it’s time to create Yew components that will utilize them to handle the authentication aspect of the application.

Account Registration Component

src/pages/register_page.rs


use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;

use crate::api::user_api::api_register_user;
use crate::components::{form_input::FormInput, loading_button::LoadingButton};
use crate::router::{self, Route};
use crate::store::{set_page_loading, set_show_alert, Store};

use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationErrors};
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize)]

struct RegisterUserSchema {
    #[validate(length(min = 1, message = "Name is required"))]
    name: String,
    #[validate(
        length(min = 1, message = "Email is required"),
        email(message = "Email is invalid")
    )]
    email: String,
    #[validate(
        length(min = 1, message = "Password is required"),
        length(min = 6, message = "Password must be at least 6 characters")
    )]
    password: String,
    #[validate(
        length(min = 1, message = "Please confirm your password"),
        must_match(other = "password", message = "Passwords do not match")
    )]
    password_confirm: String,
}

fn get_input_callback(
    name: &'static str,
    cloned_form: UseStateHandle<RegisterUserSchema>,
) -> Callback<String> {
    Callback::from(move |value| {
        let mut data = cloned_form.deref().clone();
        match name {
            "name" => data.name = value,
            "email" => data.email = value,
            "password" => data.password = value,
            "password_confirm" => data.password_confirm = value,
            _ => (),
        }
        cloned_form.set(data);
    })
}

#[function_component(RegisterPage)]
pub fn register_page() -> Html {
    let (store, dispatch) = use_store::<Store>();
    let form = use_state(|| RegisterUserSchema::default());
    let validation_errors = use_state(|| Rc::new(RefCell::new(ValidationErrors::new())));
    let navigator = use_navigator().unwrap();

    let name_input_ref = NodeRef::default();
    let email_input_ref = NodeRef::default();
    let password_input_ref = NodeRef::default();
    let password_confirm_input_ref = NodeRef::default();

    let validate_input_on_blur = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        Callback::from(move |(name, value): (String, String)| {
            let mut data = cloned_form.deref().clone();
            match name.as_str() {
                "email" => data.email = value,
                "password" => data.password = value,
                _ => (),
            }
            cloned_form.set(data);

            match cloned_form.validate() {
                Ok(_) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .remove(name.as_str());
                }
                Err(errors) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .retain(|key, _| key != &name);
                    for (field_name, error) in errors.errors() {
                        if field_name == &name {
                            cloned_validation_errors
                                .borrow_mut()
                                .errors_mut()
                                .insert(field_name.clone(), error.clone());
                        }
                    }
                }
            }
        })
    };

    let handle_name_input = get_input_callback("name", form.clone());
    let handle_email_input = get_input_callback("email", form.clone());
    let handle_password_input = get_input_callback("password", form.clone());
    let handle_password_confirm_input = get_input_callback("password_confirm", form.clone());

    let on_submit = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        let cloned_navigator = navigator.clone();
        let cloned_dispatch = dispatch.clone();

        let cloned_name_input_ref = name_input_ref.clone();
        let cloned_email_input_ref = email_input_ref.clone();
        let cloned_password_input_ref = password_input_ref.clone();
        let cloned_password_confirm_input_ref = password_confirm_input_ref.clone();

        Callback::from(move |event: SubmitEvent| {
            let form = cloned_form.clone();
            let validation_errors = cloned_validation_errors.clone();
            let navigator = cloned_navigator.clone();
            let dispatch = cloned_dispatch.clone();

            let name_input_ref = cloned_name_input_ref.clone();
            let email_input_ref = cloned_email_input_ref.clone();
            let password_input_ref = cloned_password_input_ref.clone();
            let password_confirm_input_ref = cloned_password_confirm_input_ref.clone();

            event.prevent_default();
            spawn_local(async move {
                match form.validate() {
                    Ok(_) => {
                        let form_data = form.deref().clone();
                        let form_json = serde_json::to_string(&form_data).unwrap();
                        set_page_loading(true, dispatch.clone());

                        let name_input = name_input_ref.cast::<HtmlInputElement>().unwrap();
                        let email_input = email_input_ref.cast::<HtmlInputElement>().unwrap();
                        let password_input = password_input_ref.cast::<HtmlInputElement>().unwrap();
                        let password_confirm_input = password_confirm_input_ref
                            .cast::<HtmlInputElement>()
                            .unwrap();

                        name_input.set_value("");
                        email_input.set_value("");
                        password_input.set_value("");
                        password_confirm_input.set_value("");

                        let res = api_register_user(&form_json).await;
                        match res {
                            Ok(_) => {
                                set_page_loading(false, dispatch.clone());
                                set_show_alert(
                                    "Account registered successfully".to_string(),
                                    dispatch,
                                );
                                navigator.push(&router::Route::LoginPage);
                            }
                            Err(e) => {
                                set_page_loading(false, dispatch.clone());
                                set_show_alert(e.to_string(), dispatch);
                            }
                        };
                    }
                    Err(e) => {
                        validation_errors.set(Rc::new(RefCell::new(e)));
                    }
                }
            });
        })
    };

    html! {
    <section class="py-8 bg-ct-blue-600 min-h-screen grid place-items-center">
      <div class="w-full">
        <h1 class="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
         {" Welcome to CodevoWeb!"}
        </h1>
        <h2 class="text-lg text-center mb-4 text-ct-dark-200">
          {"Sign Up To Get Started!"}
        </h2>
          <form
            onsubmit={on_submit}
            class="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Full Name" name="name" input_ref={name_input_ref} handle_onchange={handle_name_input}  errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput label="Email" name="email" input_type="email" input_ref={email_input_ref} handle_onchange={handle_email_input}  errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput label="Password" name="password" input_type="password" input_ref={password_input_ref} handle_onchange={handle_password_input}  errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput
              label="Confirm Password"
              name="password_confirm"
              input_type="password"
              input_ref={password_confirm_input_ref}
              handle_onchange={handle_password_confirm_input}
              errors={&*validation_errors}
              handle_on_input_blur={validate_input_on_blur.clone()}
            />
            <span class="block">
              {"Already have an account?"} {" "}
            <Link<Route> to={Route::LoginPage} classes="text-ct-blue-600">{"Login Here"}</Link<Route>>
            </span>
            <LoadingButton
              loading={store.page_loading}
              text_color={"text-ct-blue-600"}
            >
             {" Sign Up"}
            </LoadingButton>
          </form>
      </div>
    </section>
    }
}

Account Login Component

src/pages/login_page.rs


use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;

use crate::api::user_api::api_login_user;
use crate::components::{form_input::FormInput, loading_button::LoadingButton};
use crate::router::{self, Route};
use crate::store::{set_page_loading, set_show_alert, Store};

use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationErrors};
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize)]

struct LoginUserSchema {
    #[validate(
        length(min = 1, message = "Email is required"),
        email(message = "Email is invalid")
    )]
    email: String,
    #[validate(
        length(min = 1, message = "Password is required"),
        length(min = 6, message = "Password must be at least 6 characters")
    )]
    password: String,
}

fn get_input_callback(
    name: &'static str,
    cloned_form: UseStateHandle<LoginUserSchema>,
) -> Callback<String> {
    Callback::from(move |value| {
        let mut data = cloned_form.deref().clone();
        match name {
            "email" => data.email = value,
            "password" => data.password = value,
            _ => (),
        }
        cloned_form.set(data);
    })
}

#[function_component(LoginPage)]
pub fn login_page() -> Html {
    let (store, dispatch) = use_store::<Store>();
    let form = use_state(|| LoginUserSchema::default());
    let validation_errors = use_state(|| Rc::new(RefCell::new(ValidationErrors::new())));
    let navigator = use_navigator().unwrap();

    let email_input_ref = NodeRef::default();
    let password_input_ref = NodeRef::default();

    let validate_input_on_blur = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        Callback::from(move |(name, value): (String, String)| {
            let mut data = cloned_form.deref().clone();
            match name.as_str() {
                "email" => data.email = value,
                "password" => data.password = value,
                _ => (),
            }
            cloned_form.set(data);

            match cloned_form.validate() {
                Ok(_) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .remove(name.as_str());
                }
                Err(errors) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .retain(|key, _| key != &name);
                    for (field_name, error) in errors.errors() {
                        if field_name == &name {
                            cloned_validation_errors
                                .borrow_mut()
                                .errors_mut()
                                .insert(field_name.clone(), error.clone());
                        }
                    }
                }
            }
        })
    };

    let handle_email_input = get_input_callback("email", form.clone());
    let handle_password_input = get_input_callback("password", form.clone());

    let on_submit = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        let store_dispatch = dispatch.clone();
        let cloned_navigator = navigator.clone();

        let cloned_email_input_ref = email_input_ref.clone();
        let cloned_password_input_ref = password_input_ref.clone();

        Callback::from(move |event: SubmitEvent| {
            event.prevent_default();

            let dispatch = store_dispatch.clone();
            let form = cloned_form.clone();
            let validation_errors = cloned_validation_errors.clone();
            let navigator = cloned_navigator.clone();

            let email_input_ref = cloned_email_input_ref.clone();
            let password_input_ref = cloned_password_input_ref.clone();

            spawn_local(async move {
                match form.validate() {
                    Ok(_) => {
                        let form_data = form.deref().clone();
                        set_page_loading(true, dispatch.clone());

                        let email_input = email_input_ref.cast::<HtmlInputElement>().unwrap();
                        let password_input = password_input_ref.cast::<HtmlInputElement>().unwrap();

                        email_input.set_value("");
                        password_input.set_value("");

                        let form_json = serde_json::to_string(&form_data).unwrap();
                        let res = api_login_user(&form_json).await;
                        match res {
                            Ok(_) => {
                                set_page_loading(false, dispatch);
                                navigator.push(&router::Route::ProfilePage);
                            }
                            Err(e) => {
                                set_page_loading(false, dispatch.clone());
                                set_show_alert(e.to_string(), dispatch);
                            }
                        };
                    }
                    Err(e) => {
                        validation_errors.set(Rc::new(RefCell::new(e)));
                    }
                }
            });
        })
    };

    html! {
    <section class="bg-ct-blue-600 min-h-screen grid place-items-center">
      <div class="w-full">
        <h1 class="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
          {"Welcome Back"}
        </h1>
        <h2 class="text-lg text-center mb-4 text-ct-dark-200">
          {"Login to have access"}
        </h2>
          <form
            onsubmit={on_submit}
            class="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Email" name="email" input_type="email" input_ref={email_input_ref} handle_onchange={handle_email_input} errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput label="Password" name="password" input_type="password" input_ref={password_input_ref} handle_onchange={handle_password_input} errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()}/>

            <div class="text-right">
              <a href="#">
                {"Forgot Password?"}
              </a>
            </div>
            <LoadingButton
              loading={store.page_loading}
              text_color={"text-ct-blue-600"}
            >
              {"Login"}
            </LoadingButton>
            <span class="block">
              {"Need an account?"} {" "}
              <Link<Route> to={Route::RegisterPage} classes="text-ct-blue-600">{ "Sign Up Here" }</Link<Route>>
            </span>
          </form>
      </div>
    </section>
    }
}

Add Logout Logic to the Header Component

With the user registration and login functionalities being handled by the components, it’s time to add the previously omitted logout logic to the Header component. We can achieve this by updating the header.rs file as follows.

src/components/header.rs


use crate::{
    api::user_api::api_logout_user,
    router::{self, Route},
    store::{set_auth_user, set_page_loading, set_show_alert, Store},
};
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[function_component(Header)]
pub fn header_component() -> Html {
    let (store, dispatch) = use_store::<Store>();
    let user = store.auth_user.clone();
    let navigator = use_navigator().unwrap();

    let handle_logout = {
        let store_dispatch = dispatch.clone();
        let cloned_navigator = navigator.clone();

        Callback::from(move |_: MouseEvent| {
            let dispatch = store_dispatch.clone();
            let navigator = cloned_navigator.clone();
            spawn_local(async move {
                set_page_loading(true, dispatch.clone());
                let res = api_logout_user().await;
                match res {
                    Ok(_) => {
                        set_page_loading(false, dispatch.clone());
                        set_auth_user(None, dispatch.clone());
                        set_show_alert("Logged out successfully".to_string(), dispatch);
                        navigator.push(&router::Route::LoginPage);
                    }
                    Err(e) => {
                        set_show_alert(e.to_string(), dispatch.clone());
                        set_page_loading(false, dispatch);
                    }
                };
            });
        })
    };

    html! {
        <header class="bg-white h-20">
        <nav class="h-full flex justify-between container items-center">
          <div>
            <Link<Route> to={Route::HomePage} classes="text-ct-dark-600">{"CodevoWeb"}</Link<Route>>
          </div>
          <ul class="flex items-center gap-4">
            <li>
              <Link<Route> to={Route::HomePage} classes="text-ct-dark-600">{"Home"}</Link<Route>>
            </li>
            if user.is_some() {
               <>
                <li>
                  <Link<Route> to={Route::ProfilePage} classes="text-ct-dark-600">{"Profile"}</Link<Route>>
                </li>
                <li
                  class="cursor-pointer"
                >
                  {"Create Post"}
                </li>
                <li class="cursor-pointer" onclick={handle_logout}>
                  {"Logout"}
                </li>
              </>

            } else {
              <>
                <li>
                  <Link<Route> to={Route::RegisterPage} classes="text-ct-dark-600">{"SignUp"}</Link<Route>>
                </li>
                <li>
                  <Link<Route> to={Route::LoginPage} classes="text-ct-dark-600">{"Login"}</Link<Route>>
                </li>
              </>
            }
          </ul>
        </nav>
      </header>
    }
}

After the user clicks on the logout button in the navigation menu, the handle_logout callback is activated, which calls the api_logout_user() function to submit the logout request to the backend API.

If the request is successful, the user will be directed to the login page. But if it fails, a notification toast will be displayed to present the error message received from the backend server.

Create the Remaining Page Components

In order to further illustrate the process of restricting access to protected pages and automatically refreshing expired access tokens, we will now create the remaining Yew components.

Home Page

This component will serve as the Home page and it will display a simple message in the UI when the user lands on the root route. To create this component, create a home_page.rs file in the ‘pages‘ directory and add the following code.

src/pages/home_page.rs


use crate::components::header::Header;
use yew::prelude::*;

#[function_component(HomePage)]
pub fn home_page() -> Html {
    html! {
      <>
        <Header />
        <section class="bg-ct-blue-600 min-h-screen pt-20">
            <div class="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
                <p class="text-3xl font-semibold">{"Welcome to Rust, Yew.rs and WebAssembly"}</p>
            </div>
        </section>
      </>
    }
}

Profile Page

To showcase the functionality of protected pages and demonstrate how access tokens can be automatically refreshed in the background, let’s create a profile page that displays the authenticated user’s profile information. After successful authentication, the login component will automatically redirect the user to the profile page.

When the user lands on the profile page, the use_effect_with_deps hook will trigger the asynchronous code within the wasm_bindgen_futures::spawn_local() method to fetch the user’s credentials using the api_user_info() method.

If the request resolves successfully, the user’s profile information will be displayed in the UI. However, if the backend server returns an error message “You are not logged in“, it means that the access token has expired, and the api_refresh_access_token() API function will be called to request a new access token from the backend API.

If a new access token is retrieved, the api_user_info() function will be called again to retrieve the user’s information using the new access token. However, if no access token is returned, the user will be redirected to the login page to re-authenticate.

To create this component, create a profile_page.rs file in the ‘pages‘ directory and add the following code.

src/pages/profile_page.rs


use crate::{
    api::user_api::{api_refresh_access_token, api_user_info},
    components::header::Header,
    router,
    store::{set_auth_user, set_page_loading, set_show_alert, Store},
};
use yew::prelude::*;
use yew_router::prelude::use_navigator;
use yewdux::prelude::*;

#[function_component(ProfilePage)]
pub fn profile_page() -> Html {
    let (store, dispatch) = use_store::<Store>();
    let user = store.auth_user.clone();
    let navigator = use_navigator().unwrap();

    use_effect_with_deps(
        move |_| {
            let dispatch = dispatch.clone();
            wasm_bindgen_futures::spawn_local(async move {
                set_page_loading(true, dispatch.clone());
                let response = api_user_info().await;

                match response {
                    Ok(user) => {
                        set_page_loading(false, dispatch.clone());
                        set_auth_user(Some(user), dispatch);
                    }
                    Err(e) => {
                        set_page_loading(false, dispatch.clone());

                        if e.contains("You are not logged in") {
                            set_page_loading(true, dispatch.clone());
                            let token_response = api_refresh_access_token().await;

                            match token_response {
                                Ok(_) => {
                                    set_page_loading(true, dispatch.clone());
                                    let user_response = api_user_info().await;

                                    match user_response {
                                        Ok(user) => {
                                            set_page_loading(false, dispatch.clone());
                                            set_auth_user(Some(user), dispatch.clone());
                                        }
                                        Err(e) => {
                                            set_page_loading(false, dispatch.clone());
                                            set_show_alert(e.to_string(), dispatch.clone());
                                            navigator.push(&router::Route::LoginPage);
                                        }
                                    }
                                }
                                Err(e) => {
                                    set_page_loading(false, dispatch.clone());
                                    set_show_alert(e.to_string(), dispatch.clone());
                                    navigator.push(&router::Route::LoginPage);
                                }
                            }

                            return;
                        }
                        set_show_alert(e.to_string(), dispatch);
                        navigator.push(&router::Route::LoginPage);
                    }
                }
            });
        },
        (),
    );

    html! {
    <>
      <Header />
      <section class="bg-ct-blue-600 min-h-screen pt-20">
        <div class="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
          <div>
            <p class="text-5xl font-semibold">{"Profile Page"}</p>
            if let Some(user) = user {
              <div class="mt-8">
                <p class="mb-4">{format!("ID: {}", user.id)}</p>
                <p class="mb-4">{format!("Name: {}", user.name)}</p>
                <p class="mb-4">{format!("Email: {}", user.email)}</p>
                <p class="mb-4">{format!("Role: {}", user.role)}</p>
              </div>
            }else {
              <p class="mb-4">{"Loading..."}</p>
            }
          </div>
        </div>
      </section>
    </>
    }
}

Export the Page Components

Now that we’ve completed the authentication portion of the application, we can move on to exporting the page components as modules. To do this, we’ll create a mod.rs file in the ‘pages‘ directory and add the following code to export the page components.

src/pages/mod.rs


pub mod home_page;
pub mod login_page;
pub mod profile_page;
pub mod register_page;

Setup Routes For the Yew Components

Now that all the necessary pages have been created, the next step is to define routes for them using the yew-router crate. To achieve this, create a router.rs file in the ‘src‘ directory and add the following code to it.

src/router.rs


use yew::prelude::*;
use yew_router::prelude::*;

use crate::pages::{
    home_page::HomePage, login_page::LoginPage, profile_page::ProfilePage,
    register_page::RegisterPage,
};

#[derive(Clone, Routable, PartialEq)]
pub enum Route {
    #[at("/")]
    HomePage,
    #[at("/register")]
    RegisterPage,
    #[at("/login")]
    LoginPage,
    #[at("/profile")]
    ProfilePage,
}

pub fn switch(routes: Route) -> Html {
    match routes {
        Route::HomePage => html! {<HomePage/> },
        Route::RegisterPage => html! {<RegisterPage/> },
        Route::LoginPage => html! {<LoginPage/> },
        Route::ProfilePage => html! {<ProfilePage/> },
    }
}

Next, we’ll create the App component which serves as the main entry point for our application. To handle navigation using the yew_router library, we’ll wrap the entire app with the BrowserRouter component and use the Switch component to render the appropriate page based on the current route. To get started, create an app.rs file in the ‘src‘ directory and add the following code:

src/app.rs


use crate::components::{
    alert::{AlertComponent, Props as AlertProps},
    spinner::Spinner,
};
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::use_store;

use crate::router::{switch, Route};
use crate::store::Store;

#[function_component(App)]
pub fn app() -> Html {
    let (store, _) = use_store::<Store>();
    let message = store.alert_input.alert_message.clone();
    let show_alert = store.alert_input.show_alert;
    let is_page_loading = store.page_loading.clone();

    let alert_props = AlertProps {
        message,
        delay_ms: 5000,
    };
    html! {
        <BrowserRouter>
                <Switch<Route> render={switch} />
                if show_alert {
                    <AlertComponent
                        message={alert_props.message}
                        delay_ms={alert_props.delay_ms}
                     />
                }
                if is_page_loading {
                    <div class="pt-4 pl-2 top-[5.5rem] fixed">
                        <Spinner width={"1.5rem"} height={"1.5rem"} color="text-ct-yellow-600" />
                    </div>
                }
        </BrowserRouter>
    }
}

Render the Yew Application in the Main File

To finalize the application, we’ll need to import all the required modules at the beginning of the main file. Once we’ve done that, we can use Yew’s Renderer struct and the render() method inside the main() function to display the Yew application in the user’s browser window.

src/main.rs


mod api;
mod app;
mod components;
mod pages;
mod router;
mod store;

fn main() {
    yew::Renderer::<app::App>::new().render();
}

That’s it! You’re now ready to serve the Yew application using the Trunk development server. Simply run the command trunk serve --port 3000 and you’re good to go.

Test the RS256 JWT Yew.rs Frontend App

We’re now ready to test the application and see if the authentication flow works as expected. To interact with the application, visit http://localhost:3000. However, before you can do this, make sure to start the backend API by following the instructions provided in the ‘Run the Frontend App with a Backend API‘ section.

Home Page

Upon arriving at the root route, the homepage will be displayed. To access the account registration page, click on the “SignUp” link.

Home Page of the Rust Yew.rs Web App SignUp and SignIn Frontend

Create a New Account

On the account registration page, provide your credentials in the form and click on the “Sign Up” button to submit the form data to the backend API.

Rust RS256 JWT Authentication - Register New User

The backend server will validate your credentials upon receiving the request, add them to the database and return a successful response to the Yew app.

Once the Yew app receives a successful response from the backend API, it will redirect you to the account login page.

Sign Into the Yew App

After navigating to the login page, enter the credentials you used when creating the account and click on the “Login” button to sign in. This will trigger the Yew app to add the credentials to the request body and send them to the backend API.

Rust RS256 JWT Authentication - Login User

The backend API will authenticate the credentials by performing a series of checks and send back cookies if the credentials are valid.

View the Cookies Sent by the Rust API in the Browser Dev Tools

If the authentication request is successful, you will be redirected to the profile page where you can view your account details. However, if the request fails, a toast notification will display the error message returned by the server.

View Account Credentials

Once you arrive on the profile page, the Yew app will send a GET request to the backend API to fetch your account details. Since this route is protected, the browser will send the stored cookies containing the access token to the backend API for authentication.

If the backend API successfully authenticates the request, it will return your account information, which the Yew app will display on the page. However, if the request fails, the app will redirect you back to the login page.

Rust RS256 JWT Authentication - Access Profile Detials

To demonstrate how the access token can be refreshed, you can simulate its expiration by deleting the access token cookie in your browser’s dev tools. To do this, go to the “Application” tab, find the Cookies section, and locate the access token for http://localhost:3000/. Then, delete the access token.

Once you’ve deleted the access token, refresh the profile page. Within a few milliseconds, the Rust API will send a new access token, which the Yew app will use to retrieve your account information.

To log out, simply click the “Logout” link on the navbar. If the logout request is successful, all the cookies obtained during the login and refresh process will be deleted, and you will be redirected to the login page.

Conclusion

And there you have it! You can find the full source code of the Yew application on GitHub.

In this tutorial, we built a Rust frontend application with the Yew framework to enable user registration, login, and logout. We also went a step further by demonstrating how to retrieve access to a protected page and refresh the access token in the background when it expires.

I hope you found this tutorial useful. If you have any questions or feedback, don’t hesitate to leave a comment below.