Hello World using React, Rust, and WebAssembly

In recent years WebAssembly has been gaining more popularity. As a binary format designed to be a compile target for the web, WebAssembly allows C, C++ and Rust programs to compile and run on websites....

4 months ago

Latest Post Kubernetes Cheat Sheet by Tyler Moon

In recent years WebAssembly has been gaining more popularity. As a binary format designed to be a compile target for the web, WebAssembly allows C, C++ and Rust programs to compile and run on websites. There are many benefits to this approach but one of the primary ones is the performance and parallel processing improvements.

WebAssembly (AKA WASM) is already in a binary format. There is no longer a need for the browser to parse the code which can lead to performance boosts. Rust is a systems level programming language that has also been gaining popularity recently. For more information on how to get started with Rust and setup the rustc compiler check out my article here.

React is a JavaScript library build for creating intuitive user interfaces. Initially developed by Facebook, React uses a declarative, component-based approach to front-end JavaScript which is easy to read and debug.

In this article, we are going to cover how to set up a skeleton Hello World site using these three technologies. This will be a very simple site and we will use an Express.js static server for development. Any web server that can render static HTML would work, Express is a minimal way of doing so without a lot of overhead.

Prerequisites

Project Setup

Create a new root project directory webassembly_rust_react_helloworld. In that directory run the following NPM command to generate a new package.json file.

npm ini -y

Now open up the newly created package.json file and add these NPM dependencies

"devDependencies": {
    "@babel/core": "^7.5.0",
    "@babel/preset-env": "^7.5.0",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.6",
    "babel-preset-react": "^6.24.1",
    "webpack": "^4.35.2",
    "webpack-cli": "^3.3.5"
  },
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }

And now run the NPM install command below to install webpack, babel, react, and express

npm install

Here is a quick rundown of what those NPM packages do:

To finish out setting up this project structure create the following files. We will fill them out in the next section so just leave them blank for now.

/
~ .babelrc // Babel config file
~ Cargo.toml // Rust package config file
~ package.json // Should have been created by the npm init -y command above
~ server.js // Express.js server file
~ webpack.config.js // Webpack config file
/src/index.js // Main React file
/src/lib.rs // Main Rust file
/dist/index.html // Main HTML file

Fill out the source code

Let's start with the standard front end code. Open up the dist/index.html file and add the following boilerplate React HTML bootstrap code.

<!doctype html>
<html>
    <head>
        <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
        <title>React, Rust, and WebAssembly</title>
    </head>
    <body>
        <!-- Content root where React will be injected -->
        <div id="root"></div>
        <!-- Result of the webpack build of the WASM modules -->
        <script src="/bundle.js"></script>
    </body>
</html>

This is our main HTML file in which we will render the React components by adding them to the <div id="root"></div> line. The <script src="/bundle.js"></script> line will load in the result of Webpack "bundling" our React code.

Add the following to the src/index.js file to create the before mentioned React component.

// Import react packages
import React from "react";
import ReactDOM from "react-dom";

// Import our WebAssembly binary
const wasm = import("../build/react_rust_wasm");

// Call the WebAssembly method
wasm.then(wasm => {
  // Construct a React component called "App"
  const App = () => {
    return (
      // Return some html which calls the rust method "rustAlert" on a button click
      <div>
        <h1>Hi there</h1>
        <button onClick={wasm.rust_alert}>Alert</button>
      </div>
    );
  };
  // Render the newly created App component to the root div in the index.html file created earlier
  ReactDOM.render(<App />, document.getElementById("root"));
});

This will create the React component and add it to the "root" div element that we defined in the index.html file. It will call a WebAssembly method "rust_alert" on a button click event.

Now we need to create the Rust code that will run on the button click. Add the following to the src/lib.rs Rust file.

// Use external crate for WASM integration
extern crate wasm_bindgen;

// Import from the prelude directory
use wasm_bindgen::prelude::*;

// Use the WASM package to be able to call the JavaScript alert method from rust
#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

// Setup the button click method to use the JavaScript alert to display a message to the user
#[wasm_bindgen]
pub fn rust_alert() {
    alert("Hello World from Rust!");
}

In this article, we are not going to dive into the details of Rust. The general idea here is we are loading in the WASM package that allows us to call JavaScript methods from WebAssembly / Rust. In this instance, we are calling the JavaScript alert method to display a dialog to the user.

While you can use any web server to host this simple example, it will run out of vanilla HTML and JavaScript, I would recommend setting up a simple Express.js server for this project. Express makes it simple to spin up a simple web server, API, or middleware project and is perfect for small projects like this. Add the following to the server.js file to configure the server.

// Import NPM packages
var path = require('path');
var express = require('express');

// Create an express app
var app = express();

// Set the static path to the dist folder where our compiled project will be
var staticPath = path.join(__dirname, '/dist');

// Use the static path to load static files
app.use(express.static(staticPath));

// Start listening on port 3000 for requests
app.listen(3000, function() {
  console.log('listening');
});

That is all the source code needed! We have our basic HTML boilerplate, React component for the UI piece, and our Rust code for the backing. The last step to this project is to finish all the config files so that we can easily compile and run all this code.

Fill out the config files

The first thing we are going to want to configure is compiling the Rust code. If you do not already have the Rust compiler setup head over to this article here. Edit the Cargo.toml file and add the following.

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

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

[dependencies]
wasm-bindgen = "0.2"

This file acts similar to the package.json file for NPM and tells the Rust package manager, Cargo, what dependencies and libraries to load. Run the following commands to set the Rust compiler up and install the dependencies we just defined.

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

Edit the package.json file and replace the "scripts" object with the following.

  "scripts": {
    "build-wasm": "cargo build --target wasm32-unknown-unknown",
    "build-bindgen": "wasm-pack build --out-dir build --no-typescript",
  },

Running npm run build-wasm should build the rust files now and npm run build-bindgen should compile the WebAssembly into the build directory.

Note: By default, the wasm-pack build command outputs both JavaScript and TypeScript. Here we disabled the TypeScript output with the --no-typescript flag because we are not going to use it.

Now to finish off the React part of the project add the following to the .babelrc file.

{
	"presets": ["@babel/env", "@babel/react"]
}

And then this to the 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"
};

Running npx webpack should now build the React part of the project. With the React, HTML, Rust, and WebAssembly pieces all working its time to put it all together and render it with our Express server!

Update the "scripts" object in the package.json file with the following.

  "scripts": {
    "build-wasm": "cargo build --target wasm32-unknown-unknown",
    "build-bindgen": "wasm-pack build --out-dir build --no-typescript",
    "build": "npm run build-wasm && npm run build-bindgen && npx webpack"
  },

This last build command combines the other two with the npx command we just ran. With all three together we can finally compile the whole project with just one command. Run npm run build to compile the Hello World program, then run node server.js to start the Express server. If all went well navigate to localhost:3000/index.html and you should see something like the following.

React, Rust, WebAssembly Hello World

Clicking on the "Alert" button should display a dialog like the following (different browsers and operating systems will make the dialog appear different).

Hello World from Rust!

Summary

And there you go all that effort just to get a Rust method to run in the browser! While this example is trivial and you could have accomplished the same thing with an embedded script tag in the index.html file, this gives the skeleton of a project that can use the performance, scale, and processing benefits of WebAssembly with Rust.

Tyler Moon

Published 4 months ago

Comments?

Leave us your opinion.