Reactive UI Components in Rust

Component lifecycle — Comprised of a “view” node, pointing to and action node titled “JavaScript” with an arrow titled “user event.” The action node points to a “calculate” node with an arrow titled “pass data to rust.” The “calculate” node points to a “render” node, with an arrow titled “feed new state to render.” The render node points back to the “view” node with an arrow titled “inject new render”
Component Lifecycle

First I’ll start off by saying, just because you can do what we’re going to talk about in this post doesn’t mean you should. This is an idea and a first step to exploring a functional approach to building reactive interfaces with Rust and WebAssembly.

The goal of this post — as with other posts I’ve written about WebAssembly in the past — is to show what we can do with WebAssembly and demonstrate that it doesn’t just have to be a tool for pulling computationally intensive algorithms out of our JavaScript, or porting games to the web.

High Level

When we load up our app, we’re kicking off our component’s reactive lifecycle by initializing it with a call to WebAssembly, from our JavaScript. On subsequent state changes — triggered by user or other external events — we’ll pass new information back through the cycle and rebuild our component in Rust.

Our state management approach is similar to that of Redux, Vuex and other Flux architectures, just on a smaller scale. Our user event triggers an action in JavaScript which tells WebAssembly we need to recalculate our state, and re-render the view. A key benefit of doing these state calculations in Rust is that the existing state never leaves our sandboxed environment; we only ever pass a reference to our Rust closure — which “closes” over the current state — to an event listener in our JavaScript.

Taking a more functional approach also means that we can avoid mutability and it doesn’t require us to update the state of long lived objects, which makes our component code much more declarative and less error prone.


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


To implement what we’ve discussed above, we’ll build out a form as our Rust UI component, and at each step, map out how it ties into the reactive lifecycle.

We’re going to follow a structure that will likely feel familiar for those coming from SPA backgrounds. We won’t worry too much about styling for now, but similar to SFCs or JSX: the “meat” of our component will group the logic away from our template, while we do our work in a single file.


Prerequisites: npm installed, rust installed, wasm-pack installed.

Generate, build and run the project:

npm init rust-webpack && npm run build && npm run start


First we’ll start off with our HTML template. Given that we don’t have a nifty SFC parser the way other template based frameworks do we’ll have to be somewhat creative; we will still need to think about manually adding event listeners to our template after it’s rendered, but conditional logic and iteration will still feel similar.

Before we create our initial template, we’ll need to complete a couple of steps:

  1. Add "Window", "Document", and "Element" to our features list for the web_sys crate, in our Cargo.toml file.
  2. Update the web_sys version to 0.3.5.
  3. Add mod form; to the import set at the top of our file.

Now we can create a file in our src/ directory, with the following content: with initial form initializer and template generator

Before we explain what’s going on here, we have to do a couple more steps to get our form template into the browser:

We’ll need to update our index.html file in the static/ directory to include the <div id=root></div> element:

index.html with a div#root element added

Next we’ll create a form.js file in the js/ directory that initializes our Rust form:

form.js calling the form initializer

And update our import in the js/index.js file:

index.js updated to import form.js

Now if we run npm run build && npm run start we should see something that looks like this in our browser:

User form — titled “User Form” with a “name” label and field and a submit button. The string in the name field is “Taylor”
User Form

Explanation: So what’s going on here? Well, in the file on line 4, we’ve created the form initializer init_form() that will accept a name: &str from our form.js file on initial render. On line 22 of we’ve created our template generator gen_template(). The template generator accepts the same arguments as our init_form() so that it can display the initial values of the form.

To break down the init_form() function: we’re using the web_sys crate to facilitate DOM interaction. WebAssembly doesn’t have direct access to the DOM, so web_sys in partnership with wasm_bindgen are generating JavaScript for us, behind the scenes that abstracts this limitation away from us. We’re first grabbing a reference to the window & document so that we can append our form to the <div id=root></div> element. We access the root element by using get_element_by_id() — a method provided to us by web_sys. The next step is to generate our template using the gen_template() function, and inject it into the root element.

Breaking down gen_template(): our template generator is simply interpolating the name argument from init_form() into a string of HTML using Rust’s !format().


Now that we’ve got our form template built out, we can add our event handlers. Similar to the way we’re managing DOM interaction in our form initializer, we’ll need to add some features to web_sys and bring in JsCast from wasm_bindgen.

  1. Add HtmlFormElement and FormData to the list of web_sys features.
  2. Add the line use wasm_bindgen::JsCast; to the top of our file.

Finally, we can add our submit handler: updated with event listener

Explanation: All code new to this file has // new code commented above it (line 22 and lines 28–51 are new).

Breaking down add_submit_handler(): the first thing we can notice is that this function accepts a web_sys::Element argument; lucky for us, our form_node declared in the init_form() function (line 13), is of that type!

Before we break down exactly what’s happening on line 42, it’s important to note that when passing callbacks to JavaScript event listeners from Rust, we are only able to use closures. There are some interesting problems that arise when we get to handling complex data structures with Rust/JavaScript event listeners because we have to use closures, but we’ll get into some of that later on.

On line 42 we’re creating a closure that accepts a web_sys::Event, retrieves the name property off of our FormData, and logs it in the console using web_sys::console.

If we submit our form, we should see something that looks like this:

User form with logged output — titled “User Form” with a “name” label and field and a submit button. The string in the name field is “Arya.” Below the form there is console log output that relates to previous submissions that is 1. “jon” 2. “sansa” and 3. “arya”
User Form with logged output

At this point, we aren’t doing anything reactive, we’re just responding to events with console logs; the interesting reactive behavior shows up in the next two phases of the lifecycle.


At this point we have a template and an event listener that responds to form submission. Right now, we are just logging that interaction in the console, but we want to build our UI in such a way that our user doesn’t need to reference the console to see their submission history — we want our user to see the history in the UI.

To do this, we first need to decide how we’re going to manage the form’s state. In a previous post, we took a more object oriented approach — for this form, we’re going to roll with something a little more functional.

The first thing we need to do is add a history argument to our template generator gen_template(). Our new function signature should look something like this: gen_template(name: &str, history: &Vec<String>). We’re choosing to use a Vec (vector) here, because we don’t have a fixed set of entries.

Our final gen_template() function should look like this:

Final code for gen_template function

From here we need to update our init_form() function to also accept a history argument. The reason for this — if not already clear— is that we’re going to need our init_form() function in our submit handler to regenerate our form once we’ve received the new submission.

Given that this is a more functional approach, we won’t be mutating a long lived data structure, or modifying the state of elements in the DOM — we will instead reconstruct / re-render our component when the state changes.

Before making our final changes to the init_form() function, we will need to add the serde-serialize feature to wasm_bindgen that will allow us to serialize and de-serialize our vector in and out of JavaScript. Update the wasm_bindgen crate import in the Cargo.toml to look like this:

wasm-bindgen = {version = "0.2.45", features = ["serde-serialize"]}

Now we’ll update our init_form() function to take a history: &JsValue argument:

Final code for init_form function

And our form.js file to pass in an initial value for the history argument:

Final code for form.js file

Explanation: What we have done in each of these files, is allow a history argument to be passed into our init_form() and gen_template() functions. Our init_form() function accepts an arbitrary &JsValue to be parsed by the wasm_bindgen into_serde() function which is made available by the serde-serialize feature.

In our template generator, we are iterating over the history vector to generate another component of the template. We then interpolate our history_template into our final output String.

In our form.js file, we are now passing an empty array as the second argument — in this location, we could also retrieve the history from the network or put in an arbitrary list of names. Something to note is that because JavaScript does not required a predefined length for its arrays, we are able to pass JavaScript array values into Rust and they can still be parsed to Rust Vecs.


Now we get to our final step; recreating the form based on the new state generated by form input. We will be working in our add_submit_handler() function to transition our web_sys::console::log_1() into new form creation with init_form(). Because we’re dealing with a Rust closure, we do have to get creative with how we pass our new state between these two functions. We have also set our init_form() history parameter to accept an &JsValue which means we will need to serialize the updated state into &JsValue before passing through.

Our final add_submit_handler() function should look like this:

Final code for add_submit_handler function

We’ll also need to pass the history argument into our add_submit_handler() function in the init_form() function. The new form_node reassignment should look like let form_node = add_submit_handler(form_node, history).

When a user is submitted, you should now be able to see them show up in a list below the form:

User form with rendered output — titled “User Form” with a “name” label and field and a submit button. The string in the name field is “Arya.” Below the form there is rendered output that relates to previous submissions that is 1. “jon” 2. “sansa” and 3. “arya”
Complete User Form

Explanation: The only change we’ve made here is to swap out our web_sys::console::log_1() out for a new form initialization. In order for our init_form() function to receive the correct arguments after we’ve pushed the new name in, we need to convert the history Vec into an &JsValue type (line 16); from here all we need to do is call the init_form() which will generate our template and add the submission handler for us.

Long Term

Now that we’ve covered a high level overview, walked through a basic form implementation and seen what this looks like in action, there are a lot of potential steps to take from here. The goal — as I stated in the introduction — of this discussion is to make Rust and WebAssembly more accessible to front-end developers and the web development world as a whole.

Based on the approach we’ve discussed, the fact that we can respond to events with fully built HTML instead of JSON or JavaScript objects, lends itself to some potentially exiting opportunities. Because the process of injecting HTML can be the same regardless of whether or not the pre-built HTML is provided by a WebAssembly module, or served by a Web Server, there is lots to be explored in the realm of hybrid SSR + reactive client side application, development.

Additionally, by rebuilding our component’s HTML on each render, we have the potential to scale this approach up to a full web application without ever needing to implement a Virtual DOM.

As it continues to mature, I believe we will see more and more that there is a much broader set of things to be done with WebAssembly and Rust, other than just moving the expensive tasks out of our JavaScript.




Software Engineer — JavaScript, Rust, WebAssembly

Love podcasts or audiobooks? Learn on the go with our new app.

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
Sean Watters

Sean Watters

Software Engineer — JavaScript, Rust, WebAssembly

More from Medium

Rust flavored performance on the web; a comparison of Python, JavaScript, and Rust

a[a[0]] = 9 doesn’t work in Rust, why?

Building a CLI from scratch with Clapv3

NodeJS Native Module vs WASM