Ep. 4 - Ditch Javascript, Let's Write Our Frontend in Rust🦀💻

Pratham Jagga - Sep 12 - - Dev Community

Having read the previous blogs in this series, you must have known that:

WASM is not only a way to build and run fast & light-weight frontend apps but also fully functional & complete frontends.

And if you doubt this statement, let's build one ourselves. Let's Gooo🚀💥

Let's Go GIF

🙌 Welcome back to the fourth episode of our journey into building a social media application using Yew, a Rust-based framework for building front-end applications (uses WASM under the hood). In the last episode, we created a basic login page for our social media app. Now, we’ll take a significant step forward by allowing users to add posts and display them using local storage.

What We Will Cover

  • Creating Routes for Navigation
  • Setting up Post Submission Form
  • Displaying Posts from Local Storage
  • Using localStorage to Persist Posts and Username

If you haven't been following along, feel free to catch up with the previous episodes:

  • Episode 1: Introduction to WebAssembly
  • Episode 2: Setting up the Yew Environment
  • Episode 3: Building a Login Page

Prerequisites

Knowledge of Rust and basic Yew concepts (covered in previous episodes).
Working Yew development environment. All the code in this blog can be used in the main.rs file. Also, make sure you have the required dependencies. This is the Cargo.toml file for our project:

[package]
name = "your_app_name"
version = "0.1.0"
edition = "2024"

[dependencies]
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
yew-router = { git = "https://github.com/yewstack/yew.git" }
web-sys = "0.3.70"
gloo-storage = "0.3.0"a
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Enter fullscreen mode Exit fullscreen mode

Without further ado, let’s dive in!

Structuring Our Application with Routes

Before we can build the functionality for adding posts, we need to define routes for navigation. We’ll create three pages:

  • Home page (which includes a login form)
  • Post creation page
  • Feed page (where users can view submitted posts)
#[derive(Routable, PartialEq, Clone, Debug)]
enum Route {
    #[at("/posts")]
    Hello,
    #[at("/world")]
    World,
    #[at("/")]
    Home,
    #[not_found]
    #[at("/404")]
    NotFound,
}
Enter fullscreen mode Exit fullscreen mode

In this enum, each variant corresponds to a different route. The Home route will display the login form, Hello is for creating posts, and World will show the list of posts.

We’ll also set up route switching logic that renders the appropriate component for each route:

fn switch(routes: &Route) -> Html {
    match routes {
        Route::Hello => html! { <Hello /> },
        Route::World => html! { <World /> },
        Route::Home => html! { <Home /> },
        Route::NotFound => html! { <h1>{ "404 - Page Not Found" }</h1> },
    }
}
Enter fullscreen mode Exit fullscreen mode

This switch function ensures that navigating to a different URL correctly renders the associated component.

Adding a Post Submission Form

Next, let’s focus on the Hello component, where users can add new posts. We’ll use Yew’s use_state hook to manage the post content and localStorage to store the post and the username.

#[function_component(Hello)]
fn hello() -> Html {
    let content = use_state(|| String::new());

    let on_content_input = {
        let content = content.clone();
        Callback::from(move |e: InputEvent| {
            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
            content.set(input.value());
        })
    };

    let on_submit = {
        let content = content.clone();
        Callback::from(move |_| {
            if let Some(storage) = window().unwrap().local_storage().unwrap() {
                if let Ok(Some(username)) = storage.get_item("username") {
                    let mut posts: Vec<Post> =
                        if let Ok(Some(posts_str)) = storage.get_item("posts") {
                            serde_json::from_str(&posts_str).unwrap_or_else(|_| vec![])
                        } else {
                            vec![]
                        };

                    let new_post = Post {
                        username: username.clone(),
                        content: (*content).clone(),
                    };
                    posts.push(new_post);

                    let posts_str = serde_json::to_string(&posts).unwrap();
                    storage.set_item("posts", &posts_str).unwrap();

                    web_sys::console::log_1(&"Post added!".into());
                }
            }
        })
    };

    html! {
        <div>
            <h1>{ "Hello" }</h1>
            <div>
                <label for="content">{ "Content: " }</label>
                <input id="content" type="text" value={(*content).clone()} oninput={on_content_input} />
            </div>
            <button onclick={on_submit}>{ "Submit" }</button>
        </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

The content state stores the current post content.
The on_content_input function captures user input from the text field.
The on_submit function handles saving the post to localStorage. It first retrieves the username from localStorage (set during login) and then stores the post as a serialized JSON string.

Displaying Posts from Local Storage

Once posts are stored, we can display them on the World page. Here’s how to retrieve and display posts from localStorage:

#[function_component(World)]
fn world() -> Html {
    let posts: Vec<Post> = {
        if let Some(storage) = window().unwrap().local_storage().unwrap() {
            if let Ok(Some(posts_str)) = storage.get_item("posts") {
                serde_json::from_str(&posts_str).unwrap_or_else(|_| vec![])
            } else {
                vec![]
            }
        } else {
            vec![]
        }
    };

    html! {
        <div>
            <h1>{ "World" }</h1>
            <h2>{ "User Posts" }</h2>
            <ul>
                { for posts.iter().map(|post| html! {
                    <li>
                        <strong>{ format!("{}: ", post.username) }</strong>
                        <span>{ &post.content }</span>
                    </li>
                })}
            </ul>
        </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

This component reads posts from localStorage, deserializes them, and then iterates through the list to display each post with the associated username and content.

Connecting Everything in the Root Component

Finally, let’s tie everything together in our App component. This component uses the Yew Router to render the correct page based on the current route.

#[function_component(App)]
fn app() -> Html {
    html! {
        <BrowserRouter>
            <Switch<Route> render={|routes: Route| switch(&routes)} />
        </BrowserRouter>
    }
}
Enter fullscreen mode Exit fullscreen mode

The App component will be rendered in the index.html file, which acts as the entry point for our application. Make sure you have a basic HTML setup in index.html like this:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Yew App</title>
    </head>
    <body>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now just do: trunk serve and we'll have our app up and running at localhost:8000:

Application Preview

With this setup, our social media app should be fully functional. You can now navigate between the pages, add posts, and view them in the feed.

So, in this episode, we extended our social media app to support post creation and viewing. By utilizing Yew, we’ve made significant progress toward building a nice little application.

I am not planning to add more functionalities to this application as of now, instead I will be implementing these concepts (WASM, Rust, Yew, etc.) in my other projects and I'll keep sharing my learnings in future blogs.

If you've followed everything till here, you deserve a clap. 😉

Clap GIF

Github Repository ⭐

Follow me on Dev.to 👍

Stay tuned and follow me here for more blogs!
👋 Adios, Amigos!!

. . . . . .
Terabox Video Player