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 basicTrait
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 returnHtml
. This is most often used in the Component'sview
function, but can be used elsewhere.html
is the procedural macro from writinghtml
markdown in Yew. If you are familiar with react this is very similar to writing jsxShouldRender
is a convenience type for theupdate
andchange
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 import
ing 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.
- Build
npm run build
- 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!