Deeply Integrating Rust Wasm and Vue

Stuart Heap
Oct 13, 2020 · 9 min read

Rust is definitely the new hotness amongst developers, having won the stack overflow most loved language year on year. I’ve been keeping an eye on it for a while, especially the web assembly possibilities. While the toolset has been steadily developing, and there are plenty of tutorials showing how to access pure functions defined in Rust from any JavaScript, integrating it at a deeper level with an established web development framework such as React or Vue is less clear.

The Motivation

At Motius, we are given a chance to explore new and emerging technologies. In our recent Motius Discovery camp I decided to try combining Rust web assembly with Vue. The idea is to have the business logic handled by Rust, giving the safety the amazing Rust compiler provides, with the established DOM manipulation of Vue. By using Vue here, we have access to the full ecosystem of styling libraries, something which is at this point still lacking from the pure Rust frameworks out there. I happen to be using Tailwind here, but there are plenty of options.

Building our playground

To get started, we first need a bundler. A dev environment with hot reloading would also be nice. My first thought was to simply use vue-cli to set up a Vue project, and add wasm compilation. Since vue-cli uses webpack, wasm-pack-plugin should allow this. Installing this and adding some basic config to vue.config.js does show some initial promise, and I’m able to run Rust code from within Vue. However, the way webpack chunks the Rust code limits the way it can be imported. Imports for Rust have to be done asynchronously, which means we can’t do them in our top level imports in a given .vue file, but rather have to do them within an async function. I don’t think this is going to give us the flexibility we need.

Motius Discovery booklet teaser

Looking around for other options led me to parcel. This promises simple Vue and Rust Wasm compatibility. So I give it a try, and immediately it looks like it’s what we need right out of the box. I can import the Rust code without any special treatment. Now that we have a toolchain, let’s start building an app.

Build a Better Beer

Rust and Vue logos

In my experience most tutorial apps with new technologies don’t have enough substance to see how the technology works in the real world — what works in a todo list, won’t necessarily work when scaling it up to a full calendar app. So I wanted to build something with a bit more substance.

Since I brew beer, I thought I would make a beer recipe app. But this is a web development article, and not a homebrewing guide, so I’ll skip over most of the details of the beer brewing process. If you’re interested though (and who isn’t interested in beer?), the fantastic how to brew will tell you everything you need to know.

Our first components will need to tell us how much sugar we can expect in the wort given the grains we intend to use. We need a form in Vue to select the malts and their amounts, as well as the amount of water we’ll use. This data needs to be passed to some structs in Rust, which define the rules for calculating the sugar content. The results should then be shown in Vue. There’s going to be a big code dump here; buckle up.

// lib.rs
use wasm_bindgen::prelude::*;
use serde_derive::Serialize;
use serde_json::json;
#[derive(Copy, Clone, Serialize)]
struct Malt {
id: &'static str,
name: &'static str,
max_yield: f32, // pt / kg / L
color: f32, // Lovibond
}
#[derive(Copy, Clone, Serialize)]
struct MaltInRecipe {
malt: Malt,
mass: f32,
}
#[wasm_bindgen]
struct Recipe {
malts_in_recipe: Vec<MaltInRecipe>,
volume: f32,
}
static malts: [Malt; 2] = [
Malt {
id: "2-row-barley",
name: "2 Row Barley",
max_yield: 370.0,
color: 2.0,
},
Malt {
id: "munich",
name: "Munich Malt",
max_yield: 350.0,
color: 10.0,
}
];
#[wasm_bindgen]
impl Recipe {
pub fn new() -> Recipe {
let volume = 23.0;
let malts_in_recipe = vec![];
Recipe {
malts_in_recipe,
volume,
}
}
pub fn get_original_gravity(&self) -> f32 {
let total_yield = self.malts_in_recipe.iter().fold(0.0, |sum, malt| sum + malt.malt.max_yield * malt.mass);
1.0 + (total_yield / self.volume) / 1000.0
}
pub fn get_volume(&self) -> f32 {
self.volume
}
pub fn set_volume(&self, value: f32) -> Recipe {
let malts_in_recipe = self.malts_in_recipe.clone();
let volume = value;
Recipe {
malts_in_recipe,
volume,
}
}
pub fn get_malts_in_recipe(&self) -> String {
json!(self.malts_in_recipe).to_string()
}
pub fn update_malt_mass(&self, index: usize, new_mass: f32) -> Recipe {
let mut malts_in_recipe = self.malts_in_recipe.clone();
let volume = self.volume;
malts_in_recipe[index].mass = new_mass; Recipe {
malts_in_recipe,
volume,
}
}
pub fn add_malt(&self, maltId: String) -> Recipe {
let mut malts_in_recipe = self.malts_in_recipe.clone();
let volume = self.volume;
return match malts.iter().find(|malt| malt.id == maltId) {
None => return Recipe {
malts_in_recipe,
volume,
},
Some(found_malt) => {
malts_in_recipe.push(MaltInRecipe {
malt: found_malt.clone(),
mass: 0.0,
});
return Recipe {
malts_in_recipe,
volume,
};
}
}
}
pub fn get_available_malts(&self) -> String {
json!(malts).to_string()
}
}
// App.vue
<template>
<div id="app">
<Form>
<Input
:id="volume"
v-model="volume"
type="number"
label="Volume"
/></Input>
<h1>{{originalGravity}}</h1>
<h2>Malts</h2>
<div v-for="(maltInRecipe, index) in maltsInRecipe">
<h3>{{maltInRecipe.malt.name}}</h3>
<Input
:id="maltInRecipe.malt.id"
@change="updateMaltMass(index, $event)"
:value="maltInRecipe.mass"
type="number"
label="Mass"
></Input>
</div>
<Select v-model="selectedMalt">
<option value=''>Select Malt</option>
<option v-for="availableMalt in availableMalts" :value="availableMalt.id">
{{availableMalt.name}}
</option>
</Select>
<Button @click="addSelectedMalt" :disabled="selectedMalt === ''">Add Malt</Button>
</Form>
</div>
</template>
<script>
import { Recipe } from './lib.rs'
import Input from './components/form/Input'
import Form from './components/form/Form'
import Select from './components/form/Select'
import Button from './components/form/Button'
export default {
components: {
Input,
Form,
Select,
Button,
},
data: () => ({
recipe: Recipe.new(),
selectedMalt: '',
}),
methods: {
updateMaltMass: function(index, event) {
this.recipe = this.recipe.update_malt_mass(index, event.currentTarget.value)
},
addSelectedMalt: function() {
this.recipe = this.recipe.add_malt(this.selectedMalt)
this.selectedMalt = ''
}
},
computed: {
volume: {
get: function() {
return this.recipe.get_volume()
},
set: function(newValue) {
this.recipe = this.recipe.set_volume(newValue)
},
},
originalGravity: function() {
return this.recipe.get_original_gravity()
},
maltsInRecipe: function() {
return JSON.parse(this.recipe.get_malts_in_recipe())
},
availableMalts: function() {
return JSON.parse(this.recipe.get_available_malts())
}
},
}
</script>

I realise that’s a lot to drop on you, but I hope it’s clear enough that most developers would be able to follow it. Clearly there’s a lot of cruft in here. The connection between Rust and JavaScript is relatively limited — functions are easy to call (as long as the return type is a simple type), but referencing properties of structs is a no-go. The easiest solution to this is to just make getter functions.

Managing the State

What we have now works, but I don’t think it’s going to scale very well. Everything is just in App.vue or lib.rs. As we keep adding more components, these will grow huge, and we can’t have that. Facebook has established a pretty solid standard for state management in the frontend development world with flux, which is also the principle behind the very popular redux and vuex. I want something along these lines, although I only have two weekends in which to make it, and one of them has been spent on initial setup, so it’s probably going to fall short. I guess I’m also going to need a name for this new stack. Since it’s a combination of flux and rust, and also a bit rough around the edges, I think I’ll go with “fluster”.

The goal is to have a global state, which can only be manipulated in controlled ways. They must be immutable, rather than changing directly, the structs will provide mutation functions which will return a new object with the changes applied. The state will be available to any component which cares to look at it. The main connection here is actually quite easy to set up. All we need to achieve all of this is our own Vue mixin:

// fluster.js
export default {
install(Vue, options) {
Vue.mixin({
data: () => ({
$state: options.state,
}),
methods: {
$update: options.update,
},
})
}
}
// main.js
...
import fluster from './fluster'
const state = {
recipe: Recipe.new(),
equipment: Equipment.new(),
}
const update = (name, value) => {
state[name] = value
}
Vue.use(fluster, {
state,
update,
})
...

I think it’s important to recognise here that this is not a full implementation of flux in Rust. It’s merely inspired by it. The most important difference is that instead of the powerful actions and dispatchers, I simply have mutation functions. I have a vague idea of how I would push it further to align better, and if there is any interest from others, maybe I’ll develop this further. But for now, this is all I have.

As a small timesaver, I also made a standardised pattern of adding a get_json function to each struct, allowing me to reduce the amount of individual getters I need to write. Now, in order to access any property from the fluster store, you can simply have a single computed value for the full object:

obj: function() {
return JSON.parse(this.$data.$state.{object}.get_json())
}

Which can then be accessed by anything else in the Vue component. Any functions on the struct can be called just as easily. As an example of a full component:

<template>
<Form heading="Equipment">
<Input
:id="efficiency"
:value="efficiency"
@change="setEfficiency"
type="number"
label="Efficiency"
step="1"
unit="%"
/></Input>
</Form>
</template>
<script>
import Input from './components/form/Input'
import Form from './components/form/Form'
import Select from './components/form/Select'
import Button from './components/form/Button'
export default {
components: {
Input,
Form,
},
methods: {
setEfficiency: function(e) {
this.$update('equipment', this.$data.$state.equipment.set_efficiency(e.target.value / 100))
},
},
computed: {
obj: function() {
return JSON.parse(this.$data.$state.equipment.get_json())
},
efficiency: function() {
return Math.round(this.obj.efficiency * 100)
},
},
}
</script>

As you can see here — individual components now have access to the globally stored Equipment state. Similar changes have been made to the earlier components dealing with the Recipe state. This works pretty well, although it doesn’t mesh very well with the 2-way data binding central to Vue. I now wonder if maybe React would have been a better fit, but here we are.

Where do we go from here?

Ultimately this shows some promise. It’s certainly not a production ready system, but I was able to more cleanly integrate the two systems over a couple of weekends than I’d expected. The Rust Wasm community has done a great job of building up the toolset. The development environment wasn’t perfect. While I did have hot reloading, changes to the Rust code would take a long time to compile. It also seemed to keep writing new files into my /tmp folder with each compilation. After a few hours of active development my hard drive would be full, and I would need to restart to clean it up. Additionally, I have so far been unable to configure jest to work with parcel, which makes testing a bit of a problem. But to be honest, I didn’t try very hard.

While it’s still clearly early days, I found it relatively easy to deeply integrate Rust and Vue. For production systems I don’t think I would recommend this deep level of integration. The toolset simply isn’t really there yet, and it’s not clear it’s being pushed by many people. There are plenty of people looking to write apps entirely in Rust (e.g. Yew), or call pure Rust functions from JavaScript. And I think the available tooling fits in well with either of these better than it does what I have here. However, for personal projects or ambitious startups willing to develop the tooling themselves, I think this pattern, or something similar to it, is worth pursuing. I think we’re very soon going to be at the point where JavaScript does not have to do any heavy lifting.

The full code can be found on GitHub.

Motius Talents Teaser

Motius.de

We are an R&D company that is specialized in the newest…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store