State Management With WebAssembly & Rust

Object Oriented State Management With WebAssembly and Rust

Rusty gear on yellowing grass with an ocean backdrop.
Photo by Erik Mclean on Unsplash

State management in any application is always a super fun problem to solve. When it comes to integrating WebAssembly with existing applications or starting from scratch with a new project, this problem gets even more interesting, but it’s not as complicated as it may seem.

Before we jump in, I do want to make it known that this may not yet be the most performant solution for general state management (“yet” is important there). Interop between JavaScript and WebAssembly still suffers some performance limitations when it comes to serialization and de-serialization of complex data structures, but it is improving and there are proposals that are on their way that could have a significant, positive impact on performance.

Jumping In

For this example, we’re going to build a super basic counter application — you’ll be able to increment and decrement the count with “+” and “-” buttons. This will cover very surface level information and basic implementation, but won’t go deep into state management patterns like “flux with Rust,” or how to build your signup form; those are posts for another time, and I do plan on covering those topics in the next little while if folks find this walk-through helpful.

High Level

State Management Diagram

Using the diagram above, we can think about our application as being three distinct parts:

  1. The View — our HTML document that a user would interact with
  2. The Link — our JavaScript code that bridges the gap between our View and our State layer
  3. The State — our Rust code that worries about application state and provides an interface for our JavaScript to read and write from

The View layer is relatively simple — a couple of buttons and a <div /> to render our counter state. The JavaScript necessary for hooking up our view to our application state is mostly generated by a Rust library called wasm_bindgen, but we will still need to utilize the generated bindings in our custom JavaScript.

The state layer is the most interesting and complex of the three, but if implemented properly, can actually provide a very clean interface through which we interact with our application state.

Initial Setup

First we’re going to create a basic vanilla JavaScript, WebAssembly & Rust project. You’ll need to make sure that you have rust installed via rustup — instructions here. You’ll also need to make sure that wasm-pack is installed — link here.

We’ll generate the project — if you have difficulty with this step, you may need to use sudo.

npm init rust-webpack counter-app

Then we’re going to build and run the project — again, may need to use sudo.

npm run build && npm run start

You should see a blank page at localhost:8080 with Hello world! logged in the console. If you take a look in the src/lib.rs file, the default project is using the web_sys and wasm_bindgen Crates (Rust libraries) to generate this message.

The Code

So now that we’ve got our project up and running, we need to actually write some code. If you’re not yet familiar with Rust, I highly recommend reading through The Book.

We’re going to use some Object Oriented Programming paradigms to start out. OOP in Rust, is an interesting animal, and isn’t necessarily the most idiomatic approach, but may be an easier transition for folks coming from OOP backgrounds. We’ll cover a more functional style in a separate post.

In our Object Oriented approach, we are going to use only Rust for state management, and won’t be using web_sys to generate our HTML via JavaScript bindings.

Rust

First let’s create a new file called counter_state.rs in our src directory:

Rust — Counter State

There’s a bit going on here —

First we’re creating a public Rust struct, then we are implementing that struct using the impl keyword.

note: all structs with JavaScript bindings generated by wasm_bindgen must use the pub keyword.

The key indicator here that we’re using OOP-style Rust, is that in our struct implementation, we are adding a public new() method which will return an instance of the previously defined CounterState struct.

In addition to the new() method, we have also exposed three other public methods: increment_counter(), decrement_counter(), and get_counter(). The counter property on the CounterState struct is private and isn’t exposed to the consumer.

Important: we will also need to add this counter_state module to our imports in the src/lib.rs file. Add the line: mod counter_state; to the top of your file below the other imports.

HTML

The next step will be to update our static/index.html file to include the <button /> elements, as well as the element where we’ll display the counter state:

HTML — Document

JavaScript

Before we can create the JavaScript glue to connect the HTML document to the Rust state we will first need to update our package.json file to provide access to our WebAssembly module by adding "wasm": "file:pkg" to our dependencies — you will also need to run npm i again.

Finally, we can add the JavaScript that will access our stateful WebAssembly module. It will go in the js/counter.js file:

JavaScript — Glue between Counter State and Document

We will also need to update our js/index.js file to import the counter.js file, instead of the pkg/index.js file:

In the counter.js file, we’re importing the CounterState JavaScript class that wasm_bindgen has generated as a binding for our Rust struct. The generated code looks like this:

wasm_bindgen Generated JavaScript Bindings

Because we now have access to this class we also have access to the public methods on the Rust struct— what we’re doing on line 3 of the counter.js file is creating an instance of the struct in WebAssembly, and assigning it to a JavaScript variable using the .new() method we created in our counter_state.rs file.

From here, we’re setting the initial text content of the #counter HTML element using the get_counter() method. We’re also adding event listeners to the <button /> elements in our HTML document, that will increment and decrement our counter’s state.

The increment_counter() and decrement_counter() methods both return the post-modification state of the private counter property, so we don’t need to use get_counter() a second time.

To validate that we’ve successfully implemented the counter we run:

npm i && npm run build && npm run start

Checkout localhost:8080 and you should see something that looks like this:

Number 42 with “+” and “-” buttons
Number 42 with “+” and “-” buttons
Number 42 with “+” and “-” buttons

Conclusion

Object Oriented state management with Rust and WebAssembly is not only very possible, it’s actually relatively straightforward to reason about. Similar to other state management solutions, you still are creating a persistent store of some kind, and making it available as a module to your renderer — but with WebAssembly modules, you can get the performance boost for computation intensive methods, added type safety, and the other features that make Rust great.

This example only covers surface level problem spaces. We’re not using complex types or having to manage serialization or de-serialization — that does make things a little more complicated, but I will be writing another post that addresses all that in the context of <form /> creation, in the coming weeks.

If you’re feeling like, “just show me the code!” you can view it here.

Software Engineer — JavaScript, Rust, WebAssembly