Setting up a React + WASM + Typescript Project
12/20/2020

I love writing rust code. Recently, I've gained interest in using Rust for web development, via web assembly (WASM). It turns out setting up WASM with React.js and TypeScript is harder than it looks—that's why I'm writing this post.

Installation

First, we need to install some tools. There's already a great resource in the rust-wasm docs, so I won't go over it.

Setting up WASM

Next, we need to set up a basic wasm app, running:

cargo generate --git https://github.com/rustwasm/wasm-pack-template

There will be a promt asking for your project name. I'll call it hello-world in this tutorial.

Let's cd into the hello-world folder and build the app:

cd hello-world
wasm-pack build

Setting up a React App

We also need to set up a react app, with create react app, called www:

npx create-react-app www --template typescript
cd www

We need to install some development dependencies:

npm i -D webpack webpack-cli copy-webpack-plugin wasm-loader ts-loader @types/react

Let's delete some things that come with create react app:

rm src/App.css src/index.css src/logo.svg

Create a file called bootstrap.ts in www:

import("./index")
  .then((_) => {})
  .catch((e) => console.error("Error importing `index.tsx`:", e));

export default 0;

Also, change the contents of App.tsx to something simpler for now:

import React from 'react';

function App() {
  return <h1>Hello, wasm!</h1>;
}

export default App;

Let's also remove the index.css import from src/index.tsx:

import React from 'react';
import ReactDOM from 'react-dom';
- import './index.css';
// ...

In the tsconfig.json file, add "noEmit": false to the "compilerOptions" section. For example:

{
  "compilerOptions": {
    /* snip */
    "noEmit": true,
    /* snip */
  },
  /* snip */
}

In www, create a file called webpack.config.js, with the following:

const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");

module.exports = {
  // set entry point to bootstrap.ts
  entry: "./src/bootstrap.ts",
  devtool: "inline-source-map",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
      {
        test: /\.wasm$/, // only load WASM files (ending in .wasm)
        // only files in our src/ folder
        include: path.resolve(__dirname, "src"),
        use: [
          {
            // load and use the wasm-loader dictionary
            loader: require.resolve("wasm-loader"),
            options: {},
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js", ".tsx"],
  },
  devServer: {
    publicPath: "/",
    contentBase: "./public",
    hot: true,
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
  plugins: [
    new CopyWebpackPlugin({ patterns: [{ from: "public/*", to: "dist" }] }),
  ],
};

Basically, this configures webpack to build our typescript app with wasm. It also copies files from public to dist directory.

Add two new entries to the package.json "scripts" section:

{
  /* snip */
  "scripts": {
    "start": "webpack serve --mode development",
    "build": "webpack --mode production",
    // ...
  }
}

In the public/index.html file, we also need to remove the references to %PUBLIC_URL% and replace them with .. Include the bundle.js file in the html too:

<html lang="en">
  <!-- Snip -->
  <head>
    <!-- Snip -->
    <link rel="icon" href="./favicon.ico" />
    <link rel="apple-touch-icon" href="./logo192.png" />
    <link rel="manifest" href="./manifest.json" />
    <!-- Snip -->
  </head>
  <!-- Snip -->
  <script src="bundle.js"></script> 
</html>

We can start the dev server with:

npm run start

Adding WASM to the React App

To let the rest of our typescript code know about our webpack module, add, in package.json:

{
  "dependencies": {
    /* snip */
    "wasm-hello": "file:../pkg",
    /* snip */
  }
}

Note that you can name the package anything. It doesn't have to be called wasm-hello. After we make a change to the dependencies object, we have to tell npm to reinstall:

npm install

Now, we need to run the greet function in the WASM module. For demonstration purposes, I'll add it to a useEffect hook, so it runs when the component is loaded:

import React, { useEffect } from "react";
import * as wasm from "wasm-hello";

function App() {
  useEffect(() => wasm.greet(), []);
  return <h1>Hello, wasm!</h1>;
}

export default App;

Finally, start the development server with:

npm run start

If everything goes to plan, you should see a popup on your browser that says Hello, hello-world!