Ok(Blog)

Rust TodoMVC

Getting started with Rust and the web

January 04, 2021

This tutorial will guide you through the basics of using Rust along with wasm and Yew to build a basic TodoMVC web app. Building on this in later tutorials to start building more complex web apps utilizing Rust for both the client and server.

Setting Everything Up

note: for this tutorial I am using Ubuntu 20.04 inside VirtualBox

First, if you do not have Rust installed install it through the recommended rustup. On *nix you can install rustup using curl in your terminal.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

As of this writing the version of rust installed is 1.49.0

Second, we need to install wasm-pack. This will help us build our rust project into a wasm module to be used in the browser.

On *nix you can install wasm-pack using curl

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Now that we have rust and wasm-pack installed we can create our new project using cargo command that comes with rust. To use yew and wasm we will need our project to be a cdylib. In your terminal navigate to where you would like to create the project. I use ~/Development. Next create the new project and give it a name.

cargo new --lib yew-todo-mvc && cd $_

note: if you have not seen the && cd $_. The $_ is a reference to the last created directory. It's the same as saying && cd yew-todo-mvc.

Now that you are in the newly created project folder you should see the standard Rust file structure for a new lib project. This includes a Cargo.toml file which is the manifest file for your project and the file which you list your project dependencies. It also creates a src folder with a lib.rs file. The entry point into our application. If you list the files showing hidden files ls -al. You may also notice it created a git repository with a .gitignore file.

For good housekeeping I like to create an .editorconfig file with the standard Rust white-spacing of 4 spaces, but feel free to set this to what you prefer. Here is the example .editorconfig file that I use (for rust projects).

# http://editorconfig.org

root = true

[*]

indent_style = space
indent_size = 2

# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

# Change these settings however you like
[*.{js,jsx,ts,tsx,css}]
indent_style = space
indent_size = 2

[*.{rs,toml}]
indent_style = space
indent_size = 4

[*.md]
trim_trailing_whitespace = false

As for editors, I'm currently using vim however, feel free to use whatever you prefer. I would however suggest installing Rust syntax highlighting, linting, and code completion for the editor of your choice. You can see a list of all these at are we (I)DE yet?.

Now that we have that out of the way let's start by editing the Cargo.toml file in the root of the project. Above [dependencies] let's add a new entry for [lib]

[lib]
crate-type = ["cdylib", "rlib"]

And a couple of dependencies we will need under the [dependencies] section

[dependencies]
yew = "0.17"
wasm-bindgen = "0.2.67"

The next section is optional. Since we are using rust we don't really need to use npm or node, but since it's a web project I like to set this up (for now). It can make it easier if you have other non-rust web assets or javascript libraries that you need to use. It also makes it easy to setup some basic build commands. If you don't already have npm or node installed you can either skip this step or install them yourself at nodejs.org or using your operating system's package manager. note: As of this writing I am using node version 10.19.0 and npm version 6.14.4

Run npm init --yes in the root of your project. This will create a basic package.json file. We will add some commands to this file later on when we start building the project.

Our First Yew App

Hello, Yew

To keep things a bit clean, let's setup a basic entry point to the yew app in our src/lib.rs file. This will likely not need to change throughout the rest of your project. Open src/lib.rs in your editor and change it to

#![recursion_limit = "512"]

use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
use yew;

mod app;

#[wasm_bindgen(start)]
pub fn run_app() -> Result<(), JsValue> {
    yew::start_app::<app::App>();

    Ok(())
}

On the first line we have #![recursion_limit = "512"] is a necessary step for now. This is because of recursive procedural macros that are used by yew (i.e. html!) If you get a compile error about the recursion limit at some point you made need to change the number "512" to something greater such as "1025", but I have not had any issues with going in larger than this.

The next 2 lines just state that we are importing some external libraries. wasm_bindgen is necessary for working with wasm-pack.

Next mod app; is us importing our local app.rs file, which we have yet to create. We will do that in the next step.

The rest of the code is basically our main function for the wasm outputted code. We are telling yew to start_app the app::App component we will be building in the next step. The run_app function just returns the Result either Ok(()) if everything is, well, Ok or a JsValue which will be an error message. If you are completely new to rust please make note there is no semi-colon on the final line Ok(()) instead of using a return statement rust will return the last line of the function (as long as it does not have a semi-colon). If the last line has a semi-colon the function will not return anything (i.e. ()). In this case since we have told rust the function will return the type Result<(), JsValue> if we had a semi-colon on the last line following Ok(()); then the compiler would let us know we have done something unexpected.

Our First Yew Component

Let's go ahead and create that src/app.rs file we referenced in our src/lib.rs file (mod app;). Open src/app.rs. At the top of the file we will add the basic yew imports that (almost) every component will need.

use yew::prelude::{
    Component,
    ComponentLink,
    Html,
    html,
    ShouldRender,
};
  • Component is the basic Trait we will need for creating a Component in yew.
  • ComponentLink is the link to a component's scope for creating callbacks within a component. Basically, this is how components will communicate with each other using messages.
  • Html is a type used for letting functions now they should return Html. This is most often used in the Component's view function, but can be used elsewhere.
  • html is the procedural macro from writing html markdown in Yew. If you are familiar with react this is very similar to writing jsx
  • ShouldRender is a convenience type for the update and change functions of a component. It is basically just a boolean value.

Let's write our first component. In the src/app.rs file under our yew imports add

pub struct App {
    link: ComponentLink<Self>,
}

impl Component for App {
    type Message = ();
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            link,
        }
    }

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

    fn view(&self) -> Html {
        html! {
           <h1>{ "Hello, Yew!" }</h1>
        }
    }
}

First, we create a new struct App and give it a field called link. This is the most basic struct needed for implementing the Component trait in yew. In the current example we are not actually using the ComponentLink, but it is currently still required. Notice also that to implement a Component we have two associated types. Message and Properties for now we are just using an empty unit () to represent these types as we aren't going to be using them just yet. The create function takes in 2 parameters. First the props (associated with the components Properties) and second a link (associated with the ComponentLink<Self>. It returns an instance of it's Self and currently only has the one field link.

The update and change functions currently are only return false as our component is simple and will not be changing. We will go more in depth with these functions later.

The view function should return Html and we are using the html! macro to do this. It's a very simple component that just renders an h1 element with the text Hello, Yew!.

With our current src/lib.rs and src/app.rs files now we should have everything we need to compiled our rust code to wasm and output some html to a browser.

Build and Run

Next we will setup some basic build and run steps, including a basic single page app (spa) server. Since we setup npm we will use the package.json file to setup our commands. If you skipped this step you can either run the commands yoruself manually or you can use a rust based solution such as a Makefile.toml and install the cargo-make utility.

Edit the package.json and add few new dependencies and commands.

  "scripts": {
    "build": "wasm-pack build --target web --out-name wasm --out-dir ./static/build",
    "start:dev": "webpack-dev-server --open"
  },
  "devDependencies": {
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }

In this example I'm using webpack as a basic spa server. (we will also add a webpack config for this). This setup is not required, you can also use something like npx serve (with spa options) or python's simpleHttpServer. These options are more light weight than webpack. The webpack setup would be good if you are already using it or other javascript heavy web apps.

The main thing here is the build command we setup. If you are skipping the optional npm stuff then you can simple run the command we have for the build script to compile your rust code into wasm. wasm-pack build --target web --out-name wasm --out-dir ./static/build. Also, notice we have told wasm-pack to put our compiled wasm files into the directory ./static/build. You can change this to whatever you like. I prefer to have it setup like this and have other static files such as index.html, images, css, js, etc in the static folder and have git ignore only the static/build folder. This is up to you how you would like to structure your app.

Next, we can create a webpack.config.js file in the root next to our package.json file (if you have gone this route).

The file will be as basic as possible.

const path = require('path');

module.exports = {
  entry: './static/main.js',
  devServer: {
    contentBase: './static',
    historyApiFallback: true,
  },
};

The historyApiFallback: true option helps us to serve a spa. We also are referencing a new file called static/main.js. Webpack needs an entry point, so we will use this for now as an empty file. Let's create our static folder

mkdir static

And a few files we'll need when servering the app.

touch static/index.html && touch statc/main.js

We can leave main.js empty. Let's edit that index.html file with a very basic html5 boilerplate

<!doctype>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Yew | Rust | Wasm</title>
  </head>
  <body>
    <main id="app"></main>
    <script type="module">
      import init from './build/wasm.js';
      init();
    </script>
  </body>
</html>

The main thing here is that we setup the <main id="app"> element this will be the root DOM element that we attach our yew app to. And we have this script tag with type="module". This allows us to only try running the code if Javascript Modules are supported by the browser. If they are not we can add some kind of fallback. Maybe we compile our rust/yew app to asm.js using emscripten separately and include it with older <script src="build/fallback.asm.js">. There are several options here.

We are importing init from the ./build/wasm.js file. This is the file that wasm-pack will create. If you set the out-dir to something else then you will need to make the same change in the import line of this file.

If you have followed along with the npm/node steps then we need to run npm install to get all the dependencies before we run the build scripts.

npm install

If you are using npm version 6 this step will also create a package-lock.json file.

  1. Build npm run build
  2. Run npm run start:dev

And with that it should compile the rust code to wasm (wasm-pack will likely also download the wasm32-unknown-unknown target). And then open a browser to localhost:8080 and you should see Hello, Yew! displayed in the browser.

The final code for this part can be found on tutorial repo branch tutorial/part-one

In the next section we will start building the TodoMVC app with client side routing!

part two