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.
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 c rate-type
, which is required by the wasm-pack
we chose earlier.
[package]
name = "todo_app_yew"
version = "0.1.0"
authors = ["Robert Sudec <robert.sudec@barrage.net>"]
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.