expand
Yew framework developer - hero

You do Yew: Rust frontend framework that compiles into WebAssembly

The big question is… what is Yew? First of all, it’s a Rust framework, so it must be a tool for developing backend projects. Not at all! Yew is a frontend framework! It uses Rust language and it compiles into WebAssembly so the application runs in the browser.

Robert Sudec May 28, 2021

Reading Time30 min

Rust and WebAssembly

The main point of this post is Yew, but as it uses Rust and WebAssembly, a few words about them won’t hurt. 

If you don’t know about Rust, and want to get familiar with it, I suggest reading our blog post about Rust programming language and The Rust Book. The book contains a lot of information and examples to get you started.

I will not bug you with big words describing Rust, let’s just say it’s a system programming language that generates machine code with full control of memory use, like C/C++, but safe. “Safe” meaning that it is hard to mess up because the compiler checks every try to access memory and this is what makes it excellent. All in all, when writing “normal” applications, you can use your programming skills in combination with Rust’s unique features (described in the Book), and I think you will be satisfied with the development processes and the result.

WebAssembly (Wasm) can be referred to as the Assembly, but for the Web. It’s a low-level language running web applications at native-like performance. Rust can compile (also C/C++) into Wasm which enables Rust applications to run on the Web.

It is efficient and fast, safe, open and debuggable, and part of the open web platform. For more info on Wasm I will refer you to check out their docs.

Yew

As I said, Yew is a Rust frontend framework for creating multi-threaded web applications with Wasm but what I haven’t mentioned are Yew’s main features. 

It is a component-based framework, making it easy to build UI, especially for developers who’ve used frameworks like React (I haven’t but it didn’t take me long to learn it). DOM API calls are expensive in this situation, and we are going to have a lot of them. Luckily, Yew minimizes those and offloads processes to the background so it achieves great performance. Wasm isn’t here to replace Javascript, it’s here to empower it. Javascript interoperability allows you to integrate your Yew app with Javascript applications.

If you like what I wrote so far, you can continue reading my Yew App Walkthrough, and if not, there are alternatives like Percy and Seed.

Simple Yew application walkthrough

The time has come for us to stop talking and start doing. In this walkthrough, I am going to try to give you a brief, but clear overview of Yew projects and concepts on the example of a simple Todo list manager application.

Setting up

You need to install some tools to get started. First of all, you need Rust and Rust’s package manager Cargo and for that, follow the Book mentioned in the introduction.

Then, it’s time to choose the Wasm build tool. There are a number of them and I used the wasm-pack which is actively maintained and easy to use. These tools are used to package a Wasm file.

Install the tool with cargo:

cargo install wasm-pack

Now we can build our app, but we won’t because we don’t have an application yet. Here we specify the target, build name, and the output directory.

wasm-pack build --target web --out-name wasm --out-dir ./static

We will also need a server that will host our application. For that, you can use any server of your choosing. I used miniserve. Here we specify the hosted directory which is the output directory from our build command and tell the server to look for the index page on start.

cargo install miniserve
miniserve ./static --index index.html

We are all set now with the tooling. Let’s get to the application.

Creating a project

Using cargo, create a new Rust library and move to the created folder. It’s important to create a library, not a binary.

cargo new --lib todo_app_yew && cd todo-app_yew

Now, we need to depend on yew and wasm-bindgen. Go to your cargo.toml file and add the dependencies. Also you need to specify crate-type, which is required by the wasm-pack we chose earlier.

[package]
name = "todo_app_yew"
version = "0.1.0"
authors = ["Robert Sudec <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
yew = { version="0.18"}
wasm-bindgen = "0.2.67"

Components

Components in Yew and other component-based frameworks are basic elements we use. Each component has its own state and its own way of presenting the state to the DOM. We can create a component by implementing the Component trait on a struct. Our Todo application will start with a root component called Model. We need a struct to keep state, and it will hold a link to itself, but also contain other information on other components (e.g. parent component).

struct Model {
    link: ComponentLink<Self>,
}

Implementing the Component trait, we actually make it a component and implement the component lifecycle. Lifecycle consists of 6 methods, and only 4 of them are used (mostly). These are create, update, change and view. Create method is called on initialization of the component and we receive Properties and ComponentLink from the parent component. Properties are optional, and we will talk about them when we use it in the application. View method allows us to attach HTML to the component which will then render according to it. Let’s also save other methods for later.

impl Component for Model {
    type Message = ();
    type Properties = ();
    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            link,
        }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        true
    }

    fn change(&mut self, _props: Self::Properties) -> ShouldRender {
        false
    }

    fn view(&self) -> Html {
        html! {
        }
    }
}

Then there’s the run_app function. We bind the start of the application using the annotation to this function and we simply mount the Model component. 

#[wasm_bindgen(start)]
pub fn run_app() {
    App::<Model>::new().mount_to_body();
}

So now if we write some HTML in the view method and run the app, it should display in the browser. But, for this walkthrough to not be exhausting, let’s think ahead and start on Routing, and we’ll explain stuff as we go.

Try to build the app using the command from the Setting up section. You should get a new folder called static with your compiled files. Add the index.html file with the following content.

<!doctype html>
<html lang="en">
   <head>
       <meta charset="utf-8">
       <title>Yew Todo App</title>
       <script type="module">
           import init from "./wasm.js"
           init()
       </script>
   </head>
   <body ></body>
</html>

Let’s make a Home component first. Make a new file, copy the template above, rename it to Home, and add some HTML, like a Welcome message. 

Now, the html! macro can take one root html element, so if you want to have a title and a RouterAnchor, you can wrap them in one div or you can use <> </> tags. Literals need to be wrapped in curly braces and quotes, and if you’re writing Rust code, then wrap it in curly braces.

html! {
    <>
        <h2> {“Welcome”} </h2>
        <p> {“This is an app made in You do Yew walkthrough”} </p>
    </>      
}

Router

We want our application to render different components based on the route (Route is a string representing everything after the domain in the URL). So, for now we want to display our Home component when the route is “ / “

Add the yew-router to the dependencies.

yew-router = "0.15.0"

We need an enum that contains all possible routes for the application. You can place it anywhere

  • maybe it’s best to put it in a folder called routes or similar. 

Here, you need to derive Switch and annotate the route like presented below.

use yew_router::Switch;

use yew_router::Switch;

#[derive(Switch, Debug, Clone)]
pub enum AppRoute {
   #[to = "/"]
   Home,
}

We have defined one possible route. Now we need to handle it in the root component. We need to add some state to the Model struct

pub struct Model {
   link: ComponentLink<Self>,
   route_service: RouteService<()>,
   route: Route<()>,
}

In the create method, we can initialize the new attributes and pass them in. We create a new RouteService and then we can call get_route to actually get one.

let route_service : RouteService<()> = RouteService::new();
let route = route_service.get_route();
      
Self {
    link,
    route_service,
    route,
}

We got the current route, and it can only be “/” because if you specify a route manually through URL, you’re going to get 404 Not found. When you enter the URL manually, the server looks for that file and it will never find it. You need to think of these routes as internal, and you can only change the route through the application. We will get to that soon.

Now, let’s show the Home component. In the view method of the root component in the html! tags, we are going to add a match clause. We take the value of self.route and match it across all routes defined in the AppRoute enum. When there’s a match, we provide the wanted component. Now you also see how to call a component, similar to HTML tags.

html! {
    {
       match AppRoute::switch(self.route.clone()){
           Some(AppRoute::Home) => html! {<Home/>},
           None => html! {}
       }
    }
}

If you think about it, we get the route from self.route, but how to change it from inside of other components. We will prepare it now for future use and while we are here also explain some other concepts.

Message, update(), Callback, RouteAgent

Messages are used to invoke some changes in state. They are like events. You fire an event, the component processes it, making changes to state according to the message, and rerendering if necessary. 

First, we need to define all possible messages in an enum. The suggested practice is that the enum is called Msg.

Create the enum in the root component. There will be only one event in the root because it only handles routing. One type of message is called RouteChanged and expects a Route contained in it. The message name is your choice. 

pub enum Msg {
   RouteChanged(Route<()>),
}

In the impl Component, we found 2 type definitions, for Message and Properties. Because we had none of them, we didn’t use them. Now we need to assign the type of Message to Msg.

Change this

type Message = ();

to this.

type Message = Msg;

Until now we didn’t use the update method because we never made any changes to the state. We see that this method accepts a mutable reference to self, meaning we can actually mutate the state, as opposed to the view method, and a Message of our type. It returns ShouldRender which is a bool. If returned true, the component rerenders. Inside we match the accepted msg and do changes based on each msg.

What is happening here is that the component gets a new message RouteChanged containing a Route(), and then the update function is called. Through the route_service we set the new route and mutate the self.route to point to a new (now current) route. Lastly, we return true so the component renders other components based on the route.

match msg {
    Msg::RouteChanged(route) => { 
        self.route_service.set_route(&route.route, ());
        self.route = route
    },
}
true

We are almost done - now what we need is a way to receive messages from the other components. Let’s add a property to the Model struct:

router_agent: Box<dyn Bridge<RouteAgent>>

and in the create method, also initialize the router_agent:

let router_agent = RouteAgent::bridge(link.callback(Msg::RouteChanged));

and of course, pass it to the Self

Let’s try to explain what we’ve done here. RouteAgent owns a RouteService so it can react when route changes from the application, or from the browser. We are using the link to register a callback. A callback is a function that will be called sometime in the future. That function will then fire the event (send a message) RouteChanged and the component will handle it accordingly.

To test this, we need another component to navigate to. Create a TodoList component that displays a title. We are going to use that component to send HTTP requests later. 

Add a route to the AppRouter where we define all possible routes. Important note here, the matching starts at the top, so you want the most specific routes at the top and the most generic at the bottom. Usually the specific route contains the generic one in itself so if you have the generic one at the top, it will match that one even though you’re trying to navigate to some specific route.

#[to = "/lists"]
   TodoLists,
#[to = "/"]
   Home,

In the root component’s view method, add the new route.

Some(AppRoute::TodoLists) => html! {<TodoList/>},
Some(AppRoute::Home) => html! {<Home/>},
None => html! {}

Only thing left to do is create a clickable element to navigate to the TodoList component. Add the RouterAnchor in the Home component’s view method. It changes the route so the callback in the root component will execute and rerender the page.

<RouterAnchor<AppRoute> route=AppRoute::TodoLists>
        <h6>{"Go to my TodoList component!"}</h6>
</RouterAnchor<AppRoute>>

Done! This should now work and we are off to connecting a component to a backend with API calls.

HTTP Requests

You would probably want to send HTTP requests and receive responses if you’re building a web application. Now, I have my own backend API for this application. Since it is not public, I created a simplified mock API just for this.

Let’s get started. We are going to use the help of some dependencies. Add them to your cargo.toml. Serde and serde_derive are used for (de)serialization, yewtil helps us with handling HTTP requests, anyhow helps with error handling.

yewtil = { git = "https://github.com/yewstack/yew", features = ["fetch"] }
serde = {version = "1.0.125", features = ["derive"]}
serde_derive = "1.0.126"
anyhow = "1.0.40"

To the TodoItem component add two more attributes. The api attribute is a Fetch wrapper that contains the request and the response and with that we can track the state of our API call. The fetch_task is an Option, as there currently may or may not be a request to fetch. We will create the TodoList type later, this will hold the data from the HTTP response.

api: Fetch<Request<Vec<TodoListApi>>, Vec<TodoListApi>>,
fetch_task: Option<FetchTask>,

On create, set them as Default and None respectively.

api: Default::default(),
fetch_task: None,

This component will only be responsible for showing the data, we will have 2 possible messages. The first one is SetApiFetchState(FetchAction) that will update our component’s state according to the FetchAction. These can be NotFetching, Fetching, Fetched, Failed. The 2nd message is GetApi and it sends the request to get data from the API.

pub enum Msg {
   SetApiFetchState(FetchAction<Vec<TodoListApi>>),
   GetApi,
}

Remember to assign the Msg to type!

Let’s handle the FetchState first. In the update method, as always, match the msg. Fetch will automatically send these messages because it contains both the request and the response.

fn update(&mut self, msg: Self::Message) -> yew::ShouldRender {
       match msg {
           Msg::SetApiFetchState(fetch_state) => {
               self.api.apply(fetch_state);
               true
           }
           Msg::GetApi => {
           }
       }
}

Fetch gives us information about its state so we can render accordingly. Let’s go into the view method and we can then match the state and show different variations of the component.  The most important one is Fetched(response), in this one we can render all our todo lists. In the Fetching state we can maybe show a loading icon, in the Failed state we can show an error. This is an easy way to handle API calls, but for now we will implement only the Fetched.

html! {
           <>
              <h3> {"My lists"} </h3>
              {
                 match self.api.as_ref().state() {
                    yewtil::fetch::FetchState::NotFetching(_) => {
                       html! {}
                    }
                    yewtil::fetch::FetchState::Fetching(_) => {
                       html! {}
                    }
                    yewtil::fetch::FetchState::Fetched(response) => {
                       html! {}
                    }
                    yewtil::fetch::FetchState::Failed(_, _) => {
                       html! {}
                    }
               }
          </>
}

We can’t do anything with this yet, so let’s implement the request. It would probably be good to separate this, create a new file api.rs. We first need a type to deserialize into, something already used, TodoListApi. It holds an id, and a title, both of type String. You build these types according to the responses of the backend, the response from the mock api will look like this:

[{"id":"1","title":"title 1"},{"id":"2","title":"title 2"},{"id":"3","title":"title 3"},{"id":"4","title":"title 4"},{"id":"5","title":"title 5"},{"id":"6","title":"title 6"},{"id":"7","title":"title 7"},{"id":"8","title":"title 8"},{"id":"9","title":"title 9"},{"id":"10","title":"title 10"}]

If we’re reading this response in code, it indeed is a vector of TodoListApi objects. Here, we can also make the code clear because building requests in the update function can be messy.

#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct TodoListApi {
   pub id: String,
   pub title: String,
}

pub struct RequestHelper {}

static BASE_URL: &str = "https://60a7bb898532520017ae4d37.mockapi.io/todo_lists";

impl RequestHelper {
   pub fn get() -> Request<Nothing> {
       Request::get(BASE_URL)
           .body(Nothing)
           .expect("Cannot build url")
   }

   pub fn delete(body: &TodoListApi) -> Request<Nothing> {
       Request::delete(format!("{}/{}", BASE_URL, body.id))
           .body(Nothing)
           .expect("Error deleting")
   }
}

This RequestHelper just makes it easier to read, nothing special. Requests are GET and DELETE, where GET will get us all of the lists, and DELETE will delete one list that we provide an id for.

With that ready, we can implement the handler for GetApi message. Switch to the update function and let’s call that API already. 

First, we will send a message to apply the new state - Fetching. Get the request from our helper we wrote earlier and create a callback which will execute once when the response arrives. We will then try to create our Vec<TodoListApi> from JSON and match that data to send the right message. 

Remember, I explained the callback function, it is not executed just yet. Now, using FetchService, we try to fetch while providing the request and the callback we defined. We will register the task to our state.

Msg::GetApi => {           
     self.link                                                                                 .send_message(Msg::SetApiFetchState(FetchAction::Fetching));

     let request = RequestHelper::get();
     let callback = self.link.callback(
        |res: Response<Json<Result<Vec<TodoListApi>, anyhow::Error>>>| {
              let Json(data) = res.into_body();
              match data {
                  Ok(d) => Msg::SetApiFetchState(FetchAction::Fetched(d)),
                  Err(_) => Msg::SetApiFetchState(FetchAction::NotFetching),
              }
         },
     );
     let task = FetchService::fetch(request, callback).unwrap();
     self.fetch_task = Some(task);
     true
}

Displaying the response in the view method. We want, for each of the TodoListApi objects from the response, to display their id and title respectively.

html! {
   <>
       <h3> {"My lists"} </h3>
       {
          match self.api.as_ref().state() {
              yewtil::fetch::FetchState::NotFetching(_) => {
                  html! {}
              }
              yewtil::fetch::FetchState::Fetching(_) => {
                  html! {}
              }
              yewtil::fetch::FetchState::Fetched(response) => {
                  html! {
                     response.iter().map(
                        |todo_list: &TodoListApi| {
                            html! {
                               <>
                           <h3>{&todo_list.id}{“|”} {&todo_list.title}</h3>
                               </> 
                            }                          
                        }
                     ).collect::<Html>()

                 }
              }
              yewtil::fetch::FetchState::Failed(_, _) => {
                  html!{}}
              }
         }
    </>
}

Rendered

Now, if you navigate to the TodoList component, you will not see anything because we never sent the GetApi message so the request was never sent and the response never arrived.

There is a function in the component lifecycle that I haven’t mentioned - the rendered function. It is called after the view function is processed. It gives us a helpful first_render value which we can use to, guess what, fetch the API when the component is loaded for the first time. We can do it by calling the update function.

fn rendered(&mut self, _first_render: bool) {
       if _first_render {
           self.update(Msg::GetApi);
       }
   }

Now it should try and fetch the data and hopefully present it to you as we described.

Properties

Until now, we didn’t have use for them, but components can use Properties to communicate. We are going to create a new component, whose purpose is to delete a list. That means every TodoList component should contain a DeleteTodoList component. For deleting, we want to know the id of the element we are deleting, so the DeleteTodoList component needs to have a binding id. We can solve this problem by using properties.

For starters, create a new component called DeleteTodoList. Because it is also calling the API, it is similar to the TodoList component. The DELETE request to the API, if successful, will return that same element we are deleting. When we were getting all elements, we expected a Vec<TodoList>, and now it will be a single TodoList. Msg is also similar, DeleteApi instead of GetApi, just to be clear. This component will show a button that sends the DeleteApi Msg when clicked on.

pub struct DeleteTodoList {
   api: Fetch<Request<TodoList>, TodoList>,
   fetch_task: Option<FetchTask>,
   link: ComponentLink<Self>,
}
pub enum Msg {
   SetApiFetchState(FetchAction<TodoList>),
   DeleteApi,
}
impl Component for DeleteTodoList {
   type Message = Msg;
   type Properties = ();

   fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
       DeleteTodoListComponent {
           api: Default::default(),
           fetch_task: None,
           link,
       }
   }

   fn update(&mut self, msg: Self::Message) -> yew::ShouldRender {
   }

   fn change(&mut self, _props: Self::Properties) -> yew::ShouldRender {
       false
   }

   fn view(&self) -> yew::Html {
       match self.api.as_ref().state() {
           yewtil::fetch::FetchState::NotFetching(_) => {
               html! {
                  
                   <button type="button" onclick=self.link.callback(|_|     Msg::DeleteApi)>
                       { "Delete" }
                   </button>
               }
           }
           yewtil::fetch::FetchState::Fetching(_) => {
               html! {}
           }
           yewtil::fetch::FetchState::Fetched(_response) => {
               html! {}
           }
           yewtil::fetch::FetchState::Failed(_, _) => {
               html! {}
           }
       }
   }

   fn rendered(&mut self, _first_render: bool) {}

   fn destroy(&mut self) {}
}

We already created the RequestHelper for deleting the element, and it takes a TodoList type, but we don’t have access to it in the DeleteTodoList component. We are going to use Properties to forward the TodoList to this component.

First, we need a new type with a (suggested) name, Props. Props contain all types that we are sending to the child component. In this case, it will be a single TodoList. Also, this struct needs to derive Properties and Clone traits.

#[derive(Properties, Clone)]
pub struct Props {
   pub todo_list: TodoList,
}

Add the props to the DeleteTodoList struct so we can access given properties and assign the type (as we did with messages) in the impl Component.

type Properties = Props;

The last step is to initialize the TodoList struct with props we got in the create method. Now we can access the props through self.props.

We need to modify the TodoList components’ view method on the Fetched event, and for each TodoList we get from the API, show a DeleteTodoList component while passing itself. Properties are sent like HTML attributes and need to be named the same as you defined in Props.

<MyComponent prop1=value1 prop2=value2 … />

yewtil::fetch::FetchState::Fetched(response) => {
    html! {
        response.iter().map(
            |todo_list: &TodoList| {
                 html! {
                     <>
                        <h3>{todo_list.id}</h3>
                        <h5>{&todo_list.title}</h5>
                        <DeleteTodoList todo_list=todo_list.clone()/>
                     </> 
                 }                          
            }
        ).collect()
   }
}

Let’s try to call the API and delete one element. We need to implement the update method. Basically the same as the GetApi from above, but with a few tweaks. We are building the delete request passing the todo_list from props. That should now draw buttons for every TodoList, but when clicked on, nothing happens. I mean, it does, in the background, but it does not refresh our list with this single element removed. If you reload, then it should be gone.

fn update(&mut self, msg: Self::Message) -> yew::ShouldRender {
       match msg {
           Msg::SetApiFetchState(fetch_state) => {
               self.api.apply(fetch_state);
               true
           }
           Msg::DeleteApi => {
               self.link.send_message(
                   Msg::SetApiFetchState(FetchAction::Fetching)
               );
               let request = RequestHelper::delete(&self.props.todo_list);
               let callback = self.link.callback(
                   |res: Response<Json<Result<TodoListApi, anyhow::Error>>>| {
                       let Json(data) = res.into_body();
                       match data {
                           Ok(d) =>                                     Msg::SetApiFetchState(FetchAction::Fetched(d)),
                           Err(_) => Msg::SetApiFetchState(FetchAction::NotFetching),
                       }
                   },
               );

               let task = FetchService::fetch(request, callback).unwrap();
               self.fetch_task = Some(task);
               true
           }
       }
   }

How do we approach this problem? We need to refresh the parent component (TodoList) when Delete gets executed successfully, and we do it by sending the GetApi message. At this moment, we can’t. The solution I used is to create a callback function in the parent that will send the message, then forward that callback to the child through properties so we can run that callback function when the delete request is done.

Go to the TodoList components’ view method, and there we can register a callback which will send the GetApi message.

let refresh_csllbsck = self.link.callback(|_| Msg::GetApi);

In DeleteTodoList,Props add the new property of type Callback<Msg>, but the Msg here is the Msg type of the parent, so make sure you use that correctly.

pub refresh: Callback<super::todo_list::Msg>

Also, we will use another message Msg::Deleted to handle the refreshing of the parent component, so add that as well. Again, the Msg we pass is the Msg of the parent.

Deleted(super::todo_list::Msg)

For the update method, handling the Msg::Deleted is as simple as it gets. We only emit() the callback from our props. We are returning false, as we don’t need this component to refresh because the parent component is going to refresh and with that, create new child components.

Msg::Deleted(msg) => {
               self.props.refresh.emit(msg);
               false
           }

Messages are now ready, and we only need to send the DeleteAPI message when the delete request is successfully executed. We have the request state handling in the view method, so maybe this is where we can invoke the message DeleteAPI when the state is Fetched? Not really, because view gets a reference to self and the update method needs a mutable reference to self and that does not work. We also have the information about fetching state when handling the SetApiFetchState message and there we can call the update method. 

So, add the following code in the SetApiFetchStatehandler. Send a Msg::Deleted, but containing the right parent message, GetApi. With this, when we delete an element we send a message to the parent through callback functions and make the parent reload itself.

match fetch_state {
    FetchAction::NotFetching => {}
    FetchAction::Fetching => {}
    FetchAction::Fetched(_) => {
           self.update(Msg::Deleted(super::todo_list::Msg::GetApi));
    }
    FetchAction::Failed(_) => {}
};

Now, don’t forget to pass the callback to the child element! 

<DeleteTodoList todo_list=todo_list.clone()    
refresh=refresh_callback.clone()/>

Try it out, it may look weird based on the response time, but it does work! 

Finishing up

You did it, you made a simple application that covers all basic concepts of Yew. Now you can try to make something out of this application, you can apply styling to make it prettier, you can expand it to do more like creating new lists, add items to list, check items as done… You could try to make your own REST API for this, rather than using mockapi.io, basically whatever comes to your mind.

This was a simplified example of how everything works. You can access my application on github.com/barrage which uses a custom backend and more features. Check that repository for reference if you get stuck with something.

If you want to learn more about Rust and Yew, or you have a project you would like to develop, don't hesitate to contact us.