Exploring the Rust Web Framework: Yew

Intro

In my last post, I gave a short overview of my plan to develop this blog. In short the TLDR was the goal here is to build a blog using Rust with the modern web framework Yew. Naturally I then disappeared for a month, but don't worry, I haven't forgotten about this.

So what then, is Yew? On the surface, Yew is yet another web framework for building webapps. Inspired by Elm, Yew uses a virtual DOM to render content to your web browser. It is Rust native meaning you can use it as part of a Rust stack. That means you get all the great features Rust provides such as memory safety. In addition to compiling to WebAssembly, provides decent performance even with limitations around the calls to the DOM due to WebAssembly's lack of direct DOM access.

For myself, Yew is an opportunity for Rust to become the centre of a full-stack toolset for Web Development. Yew is not the only choice, Dioxus has rapidly developed into a viable option alongside Yew, better performance, native desktop and mobile support, where Yew relies on a solution like Tauri. Likely topics for future discussions, but for now, Yew is a great starting point with a healthy ecosystem supporting it. So let's dive in.

Yew - Getting Started

Starting off, the initial setup of an unfamiliar framework can be daunting. Some in the community push the notion that Yew is overly complex to set up. The reality is quite the opposite. Let's assume you have Rust already installed. You know the basics about Cargo, What do we need? Something to compile to and some service to serve our website. In this case our target is WebAssembly (web32-unknown-unknown) and the common choice for serving our website is Trunk. Let's get them installed.

rustup target add wasm32-unknown-unknown
cargo install trunk

Awesome! We now have our target architecture, and a service to serve our website. But we don't have a website to deliver. Let's fix that...

cargo new my-first-blog

Update the Cargo.toml with Yew with support for client side rendering

[package]
name = "my-first-blog"
version = "0.1.0"
edition = "2021"

[dependencies]
yew = { version = "0.20", features = ["csr"] }

Next we need a root document to send to our users. Let's create something simple. Create a new file at the root of the project called index.html and fill with the following.

<!DOCTYPE html>
<html lang="en">
    <head>
    </head>
    <body></body>
</html>

Now we want to use Yew, so let's create something real basic. First we can create the root of our webapp. Open your main.rs file and add the following.

use yew::prelude::*;

#[function_component(Root)]
fn root() -> Html {
    html! {
        <h1>{"Welcome to my first blog!"}</h1>
    }
}

And finally we need to be able to use what we wrote so we need to update the main function. To do this, all we need to do is tell Rust to use Yew to render our component. In this case "Root".

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

Let's run our project using Trunk

trunk serve --open

Ok maybe that was a bit of setup. So let's walk through what we have done.

  1. Install Web Assembly as a compilation target. Our web browsers support two main languages, Javascript, and WebAssembly. In this case we care about WebAssembly. WebAssembly is a language of its own, designed to be like Assembly, but for your browser. In some ways it's like Java ByteCode. But instead of Java running your ByteCode, the browser of your choice runs your WebAssembly.

  2. Install Trunk. Trunk is our helper. It is a tool that watches our files, triggers compilation if any of our files change, and runs a web server that allows us to rapidly develop our webapp. It includes hot reloading so any change we make to our assets or code, trunk will pick up and present to our browser.

  3. Create our project. Like any rust project, we have to start somewhere, and nine times out of ten, it's here.

  4. Add Yew to our dependencies. In this case we are requesting the latest version and specifying the need for the feature csr, or Client Side Rendering. Server Side Rendering is a discussion for the future so for now this gives us everything we need to render our content in the browser.

  5. Define an HTML index file. This is the very first file sent to the browser by Trunk. Trunk makes changes to this file prior to sending it to support features such as hot reloading. There are additional changes to make to this file but for now it serves as the root html content for our webapp.

  6. Define a component to render. In this case we just want to render a simple header. But to do so we need a component to do so. We will discuss components more shortly but for this context, this is the root component with every other component rendered as a child if this component.

  7. Define the renderer. In this case this is a CSR renderer. We don't have to dive too deep into this but the TLDR is the renderer takes everything we define and applies it to the DOM. There is a lot going on here, but we don't need to be concerned about it.

Now, if we take a look at our default browser, We will see a simple web page with the words "Welcome to my first blog!". Congratulations! We have now built the foundation of a webapp. There is much that we can do from here but let's go over the basic features of Yew

Yew - Components, State, and Effect

Components

At the heart of most frontend frameworks are two key concepts: components and state management. Yew is no different. In a web application, every element can be viewed as a component or a part of one. These components, defined in Rust, vary in complexity. A common approach in web development is to use the Atomic Design methodology to break down components into modular, reusable parts. For instance, in setting up our Yew project, we established the "Root" component to act as the foundation of our application. Let's explore this further by adding a "Label" component.

#[function_component(Label)]
pub fn label(props: &ChildrenProps) -> Html {
    html!(
        <label>{props.children.clone()}</label>
    )
}

There are several elements at play here. The #[function_component(Label)] macro is a procedural macro in Rust, which essentially is code that generates more code, thus increasing developer efficiency. Here, this line is defining our Component with the name "Label". We can then call upon this component in our code with <Label>.

Next, we define our function, pub fn label(props: &ChildrenProps) -> Html. This function accepts props—properties defined by Yew to supply child components. These child components could be any content defined within our label block, such as <Label>{"Hello, I am the child of the Label"}</Label>. The function then returns Html, which is defined as a VNode.

Inside the function, we utilize the html! macro to build our HTML document. This document contains a "label" that encapsulates our child props. This is a core aspect of Yew, providing us the flexibility to define any content that produces valid HTML and adheres to Rust's strict rules about lifetimes and ownership. For instance, due to Rust's ownership rules, we need to clone the children from the props to use in our HTML because we can't move the children from the props.

The example above uses props provided by the Yew framework, but we're not restricted to these alone. We can define our own props, enabling us to share values and introduce custom logic to our components. Suppose we want the option to hide our label without removing it from the generated HTML. We can do this by defining our own props.

#[derive(Clone, PartialEq, Properties)]
pub struct LabelProps {
    pub hidden: bool,
    pub children: Children,
}

#[function_component(Label)]
pub fn label(props: &LabelProps) -> Html {
    let label_class = {
        if props.hidden {
            "hidden"
        } else {
            "visible"
        }
    };
    html!(
        <label class={label_class}>{props.children.clone()}</label>
    )
}

In the code above, we've defined our own props: a boolean that controls whether our label is visible and Children, which we need to render. These props need to implement "Properties", which we can achieve using the derive macro. We then use the boolean value to determine a class value of either "hidden" or "visible". These values can be utilized in CSS to show or hide our label, adding another layer of flexibility to our components.

State

In the dynamic environment of web development, content frequently changes over the course of a website's lifecycle on a user's system. This could be due to the dynamic loading of content or user-provided input. To manage this ever-changing content, we need to store it somewhere; this is where the concept of state comes into play.

State management in web development can be a complex affair, with several libraries offering different interfaces to interact with state. These libraries can handle various forms of state, from global in-memory state to user browser-based state that remains persistent even after the user has closed the browser. However, for the sake of simplicity, we will focus on the state implementation provided by Yew, a modern Rust framework for creating multi-threaded front-end web apps with WebAssembly.

In Yew, the default state management is quite straightforward. State is maintained at the component level. If the state's content needs to be shared, it is provided to child components through props. If a parent component needs to be notified about state changes, a callback is used. Let's illustrate this with a simple counter component:

#[function_component(Counter)]
pub fn counter() -> Html{
    let state: UseStateHandle<i32> = use_state(|| 0);

    let cloned_state = state.clone();

    let callback: Callback<MouseEvent> = Callback::from(move |_| {
        cloned_state.set(*cloned_state+1);
    });

    html!(
        <div>
            <h1>{"Counter"}</h1>
            <p>{format!("Value: {}", *state)}</p>
            <button onclick={callback}>{"+1"}</button>
        </div>
    )
}

This counter component is quite simple. We first define the state, which, for illustration purposes, is explicitly done as UseStateHandle<i32>. This state is an in-memory store containing a 32-bit integer initialized to 0. We then clone the state to satisfy Rust's ownership rules, as we need to both modify and refer to the state's content.

Next, we create a callback, which takes in a closure. This closure moves the cloned_state and updates its value to its dereferenced value plus one. Finally, we define our HTML output, which consists of a header, a paragraph to display the current value of the state, and a button with a reference to the callback. This button increments the counter when clicked.

Effects and Progressive Rendering

Progressive rendering is a crucial aspect of ensuring a pleasant user experience when loading content into a website. In essence, it involves rendering the website with the currently available content and then selectively rerendering sections as new information becomes available. This technique is particularly useful when dealing with content fetched from a server that may be dynamically selected or might take some time to load. In this blog, we've used progressive rendering for the loading of the post itself. Let's explore an example where we store data fetched from an endpoint into state.

#[derive(Clone, Default)]
pub struct Content {
    pub text: String,
}

#[function_component(ExampleComponent)]
pub fn example_component() -> Html {
    let state = use_state(|| Content::default());

    let cloned_state = state.clone();

    use_effect(move || {
        wasm_bindgen_futures::spawn_local(async move {
            let content = Request::get("/sub/path").send().await;

            match content {
                Ok(response) => match response.status() {
                    200 => cloned_state.set(Content {
                        text: response.body().unwrap().as_string().unwrap(),
                    }),
                    _ => {
                        panic!("Unexpected Response!")
                    }
                },
                Err(_) => panic!("Unexpected Error!"),
            }
        });
    });

    html!(
        <p>
            {state.text.clone()}
        </p>
    )
}

This example is slightly more complex than previous ones. We start by defining a struct, Content, which in this case holds a String. In our component, we use state to hold an instance of our Content struct, and we clone this state. Next, we encounter use_effect. Unlike a callback, which returns a callback to be passed onto a button, use_effect doesn't return anything. Instead, it's where we perform a callout to an endpoint. Finally, we output the text from our Content struct.

Delving into use_effect, we indicate that we want to take ownership (move) of any variables referenced within. Here, that's our cloned_state. We then utilize the Wasm Bindgen Futures library to define an asynchronous block for performing some async task. This task involves making an HTTP request with the reqwasm library, specifically designed for HTTP calls in a wasm runtime. We send this request to a sub-path of the current domain and wait for a response. Upon receiving a response, we either update the state with a new Content object containing the response content or panic. It's important to note that this way of mapping to a string from the content isn't optimal and a better method should be implemented, one that doesn't rely on unwrap or panic for unexpected outcomes.

The function use_effect is used to hook into the lifecycle of the component, allowing us to trigger and rerun code when changes need to be applied. In Web Assembly, we only have one thread to complete any work, so blocking our rendering while waiting for an API request to finish is not ideal. This is why we use wasm_bindgen_futures::spawn_local. It schedules a task to run when the main thread is idle, allowing efficient use of clock cycles in a single-threaded context.

However, there's an issue with this code beyond the misuse of panic and unwrap. The use_effect function is meant to hook into the lifecycle of this component, but its default behavior is to execute every time the component is rerendered. Since updating the state with a successful response from the URI triggers a rerender, we end up in an infinite loop. Although we could add another state value and a boolean "first run" to check if it's the first execution of use_effect, a cleaner solution would be to use use_effect_with_deps.

use_effect_with_deps(move |_| {
    //  Code Here
    }, ());

With use_effect_with_deps, we can specify what changes should cause the enclosed code to execute. In this case, we've specified an empty tuple, which will never change, ensuring the code runs only once and then never again. Note that this code will run once per component instance, so if you have multiple instances of the component containing use_effect_with_deps, each will execute the same code. The code will also rerun if the component is ever destroyed and recreated.

Styling

Styling has never been my favorite part of web development. Whether it's conceptualizing a functional design or dealing with the eccentricities of CSS, styling often seems more burdensome than enjoyable. However, my experience with React has introduced me to the convenience of styled components, which I've grown to appreciate.

In the context of working with Yew, I've found 'stylist-rs', a library built by a Yew developer, to be quite effective. This library, an alternative to Tailwind, lets you style components in a way that's reminiscent of styled components in React. Perhaps someday, stylist-rs might become Yew's default styling method, but for now, it's an additional tool we can employ for a more organic development of our application's style. Let's illustrate its use by enhancing the button from our previous counter example.

use stylist::yew::styled_component;
use yew::prelude::*;

#[styled_component(Counter)]
pub fn counter() -> Html {
    let stylesheet = css!(
        r#"
            background-color: blue;
            padding: 10px;
            border: none;
            color: white;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 16px;
            transition-duration: 0.4s;
            cursor: pointer;

            &:hover {
                background-color: lightblue;
            }
        "#
    );

    let state: UseStateHandle<i32> = use_state(|| 0);

    let cloned_state = state.clone();

    let callback: Callback<MouseEvent> = Callback::from(move |_| {
        cloned_state.set(*cloned_state + 1);
    });

    html!(
        <div>
            <h1>{"Counter"}</h1>
            <p>{format!("Value: {}", *state)}</p>
            <button class={stylesheet} onclick={callback}>{"+1"}</button>
        </div>
    )
}

The code above shows three crucial modifications. We replace function_component with styled_component, providing access to the stylist context, vital for the next macro, css!. This is where we outline our component's style. The encapsulated string specifies standard CSS for the component level, with the capacity to set styles for child components as needed.

Our button definition reads <button class={stylesheet} ...>, which integrates the styling directly into the HTML component. In this instance, we've crafted a blue button that transitions to light blue when hovered, but the same approach can be applied to all HTML elements, be it headers, images, or entire page layouts.

This method of embedding styling within the component eliminates the need to navigate between CSS files and components, promoting maintainability and simplifying the potential reuse of these components as libraries. This way, we can seamlessly incorporate shared components without the hassle of dealing with multiple CSS files.

Looking Ahead

Front-end web development is often a labyrinth of libraries, languages, and frameworks, each offering a unique approach to the complex landscape of the web. Among these myriad options, Rust with Web Assembly might seem like just another novel solution. However, it presents a unique opportunity that transcends the norm, particularly for use cases demanding a high standard of code quality and reliability.

Rust, with its inherent safety guarantees, coupled with rigorous testing methodologies such as Behavior-Driven Development (BDD) and Test-Driven Development (TDD), can prove pivotal in the development of critical infrastructure across diverse sectors. Yet, the challenge of crafting user interfaces remains. Here, the advantage of using the same language for both front-end and back-end development becomes evident, allowing shared implementations between the two, thereby potentially enhancing development efficiency and reducing the cognitive load of juggling multiple languages and their corresponding frameworks.

In this context, Rust and Yew, among other Rust-centric frameworks, shine as promising solutions. They provide the unique opportunity to build an entire application using a single language. Beyond the benefit of a unified language, Rust offers other compelling advantages. It enables faster application development than traditional languages like Java and C#, boasts memory safety by default, a feature not commonly seen in native languages like C++, and presents the benefits of a statically typed language with robust compile-time guarantees, a facet missing in dynamic languages such as JavaScript.

Taking our discussion about progressive rendering and component styling with Yew into account, it's clear that these capabilities further enhance Rust's appeal in front-end development. Progressive rendering allows efficient management of component lifecycle, while 'stylist-rs' facilitates maintainable CSS-in-RS styling within our components. These features elevate the process of creating visually cohesive, customizable, and high-performance web applications.

Therefore, as we navigate forward in the vast ocean of web development, Rust with Web Assembly emerges as not just another choice, but a transformative solution. As we continue to explore and leverage the power of these tools, we are certain to discover new possibilities, drive innovation, and push the boundaries of what we can achieve in web development.

Copyright 2023-2024 Ugvx.
Disclaimer: The opinions expressed on this blog are solely my own and do not reflect the views of any current, past, or future employer.