Rust, React and WebAssembly

A tutorial on how to compile Rust to WebAssembly and integrate it into a React application

In this post, we're going to show how to compile some Rust code to WebAssembly, and integrate it into a React app.

Why would we want to do this?

It has become very popular in recent years for JavaScript to be used as a compilation target. In other words, developers are writing code in other languages, and compiling that code to JavaScript.

The JavaScript can then be run in a standard web browser. CoffeeScript and TypeScript are both examples of this.

Unfortunately, JavaScript was not designed to be used like this, which presents some difficult challenges. Some smart people recognized this trend, and these challenges, and decided to make WebAssembly (aka WASM).

WebAssembly is a binary format designed from the ground up to be a compile target for the web. This makes it much easier to develop compilers than it is for JavaScript, and also opens up lots of potential performance gains.

As an example, with WASM it's no longer necessary for the browser to parse the code, because it's already in a binary format.

There are lots of ways to get started with WebAssembly, and many examples and tutorials already out there. This post is specifically targeted at React developers who have heard of Rust and/or WebAssembly, and want to experiment with including them in a React app.

I will cover only the basics, and try to keep the tooling and complexity to a minimum.

Source

Complete source code for the final running example is available on GitHub

Prerequisites

You'll first need to have Rust and node installed. They both have excellent installation documentation:

Create the React App

We'll start with a barebones React app. First, create the directory react_rust_wasm, and cd into it.

Create the following directories:

src
build
dist

Then, initialize the npm package with default options:

npm init -y

Next, install React, Babel, and Webpack:

npm install --save react react-dom
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react webpack webpack-cli

Then, create the following source files:

dist/index.html:

<!doctype html>
<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
    <title>React, Rust, and WebAssembly Tutorial</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>

src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>Hi there</h1>,
  document.getElementById('root')
);

We will also need a .babelrc file:

{
  "presets": [
    "react",
    "env",
  ],
}

And a webpack.config.js file:

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  },
  mode: 'development'
};

You should now be able to test that the React app is working. Run:

npx webpack

This will generate dist/bundle.js. If you start a web server in the dist directory you should be able to successfully serve the example content.

At this point we have a pretty minimal working React app. Let's add a button so we have a little interaction. We'll use the button to activate a dummy function that represents some expensive computation, which we want to eventually replace with Rust/wasm for better performance. Replace src/index.js with the following:

import React from 'react';
import ReactDOM from 'react-dom';

function bigComputation() {
  alert("Big computation in JavaScript");
}

const App = () => {
  return (
    <div>
      <h1>Hi there</h1>
      <button onClick={bigComputation}>Run Computation</button>
    </div>
  );
};

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Now if you should get an alert popup when you click the button, with a message indicating that the "computation" is happening in JavaScript.

Adding a Splash of Rusty WASM

Now things get interesting. In order to compile Rust to WebAssembly, we need to configure a few things.

WebAssembly Dependencies

First, we need to use Rust nightly. You can switch your Rust toolchain to nightly using the following command:

rustup default nightly

Next, we need to install the necessary tools for wasm:

rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli

Create the Rust project

In order to build the Rust code, we need to add a Cargo.toml file with the following content:

[package]
name = "react_rust_wasm"
version = "1.0.0"

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

[dependencies]
wasm-bindgen = "0.2"

You can ignore the lib section for this tutorial. Note that we have wasm-bindgen in the dependencies section. This is the Rust library that provides all the magic that makes communicating between Rust and JavaScript possible and almost painless.

Now create the source file src/lib.rs to contain our Rust code:

#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn big_computation() {
    alert("Big computation in Rust");
}

I'll break this down a bit for those who might be new to Rust.

#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

The first line is telling the Rust compiler to enable some special features to allow the WebAssembly stuff to work. These features are only available in the nightly toolchain, which is why we enabled it above.

extern crate wasm_bindgen;

This is how you include code from externally libraries (known as "crates") in Rust.

use wasm_bindgen::prelude::*;

Rust has an excellent module system to keep your code cleanly separated. This line tells the compiler that we want to be able to directly access everything in the wasm_bindgen::prelude module. Prelude modules are a convention in the Rust community. If you create a library for others to use, it's common to include a prelude module which will automatically import the most important pieces of your API, to save the user the trouble of individually importing everything.

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

The extern keyword declares a section of code which is defined outside our Rust source. In this case, the alert function is defined in JavaScript. The wasm_bindgen is invoking a Rust macro which bridges that block of JS code so it can be used from Rust. Macros in Rust are very powerful. They're similar to C/C++ macros in what they can accomplish, but much nicer to use in my experience. If you've never used C macros, you can think of a macro as a way for the compiler to transform your code or generate new code based on parameters provided at compile time. In this case, the wasm_bindgen macro takes care of generating all the plumbing between Rust and JavaScript, based on the function names we provide.

#[wasm_bindgen]
pub fn big_computation() {
    alert("Big computation in Rust");
}

This is a normal Rust function, except that once again we're using the wasm_bindgen macro to generate the plumbing. In this case, the big_computation function is being made available to be called from JavaScript. When called, this function calls the alert function, which as we saw above is defined in JavaScript. We've set this up to test the complete loop of calling from JS to Rust and back to JS.

Building

We're now ready to build everything. There are a couple stages to this. We're going to implement these as simple npm scripts. Of course there are lots of fancier ways to do this.

The first stage is to compile the Rust code into wasm. Add the following to your package.json scripts section:

"build-wasm": "cargo build --target wasm32-unknown-unknown"

If you run npm run build-wasm, you should see that the file target/wasm32-unknown-unknown/debug/react_rust_wasm.wasm has been created.

Next we need to take the wasm file, and convert it into the final form that can be consumed by JavaScript, in addition to generating the proper JS files for wrapping everything. Add the following script to package.json:

"build-bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/react_rust_wasm.wasm --out-dir build"

If you run npm run build-bindgen, you should see several files created in the build directory.

Note that wasm-bindgen even creates a react_rust_wasm.d.ts file for you in case you want to use TypeScript. Nice!

Ok, now all we need is a build script to do all the steps in order:

"build": "npm run build-wasm && npm run build-bindgen && npx webpack"

Your package.json scripts section should now look something like this:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build-wasm": "cargo build --target wasm32-unknown-unknown",
    "build-bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/react_rust_wasm.wasm --out-dir build",
    "build": "npm run build-wasm && npm run build-bindgen && npx webpack"
  },

Running npm run build should work at this point. However, we still need to modify our JavaScript code to use our wasm module instead of the JS function.

Replace src/index.js with the following:

import React from 'react';
import ReactDOM from 'react-dom';

const wasm = import("../build/react_rust_wasm");

wasm.then(wasm => {

  const App = () => {
    return (
      <div>
        <h1>Hi there</h1>
        <button onClick={wasm.big_computation}>Run Computation</button>
      </div>
    );
  }

  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
});

There are a couple important changes. First, as of this writing, you need to use the import function, rather than the normal ES6 import syntax. It has something to do with not being able to load wasm asynchronously yet. In order to use this function, we need to enable a babel plugin. Install it with the following:

npm install --save-dev babel-plugin-syntax-dynamic-import

And add it to your .babelrc:

{
  "presets": [
    "react",
    "env",
  ],
  "plugins": ["syntax-dynamic-import"]
}

The import function returns a promise. That's why we need to call wasm.then in order to kick things off.

You should now be able to successfully run npm run build. Reload dist/index.html from a web server and you'll now see a message indicating it's running from Rust. And just like that, we're done!

Where to go from here

There are a lot of exciting things happening in the world of Rust+WebAssembly. This tutorial was aimed at React developers who just want to get their feet wet. Here are a few other resources you can check out if you want to go deeper.

  • Check out this great post to get an idea of the goals and vision for Rust/WASM.

  • rustwasm/team. This seems to be the central repository for keeping up with the current state of Rust and WebAssembly. It's a fantastic resource.

  • wasm-bindgen I highly recommend reading through their documention and examples. A good chunk of this tutorial is copied almost exactly from there. There are many more advanced features that can be used, such as using other JavaScript APIs, defining structs in Rust and using them in JS, passing those structs between Rust and JS, and many more.

  • stdweb is a bridging library that has some overlap with wasm-bindgen. stdweb has some nice features and macros for letting you write JavaScript inline in your Rust code, rather than just a simple bridge. wasm-bindgen seems to be more focused on bridging, and is designed to be used with languages other than just Rust in the future.

  • Yew is a Rust framework for writing client-side apps. It's heavily inspired by React, but it lets you write your app 100% in Rust.

  • The excellent New Rustacean podcast recently did an episode on Rust/WASM. I highly recommend giving it a listen.

Download the (free) first chapter of our React Book


Anders Pitman

This post was written by Anders Pitman, who is a data visualization software engineer with a love for computer graphics and lean, fast software. He's passionate about using software to improve human health and STEM education. He can be reached at anderspitman.net