Creating a State Manager for React with Rust and WebAssembly

by Daniel Boros

Nov 12

5 min read

79 views

Introduction to WebAssembly and Why Rust Is an Excellent Choice

WebAssembly (Wasm) is a powerful binary instruction format designed to run high-performance code in the browser, allowing languages like Rust to bring native-like performance to web applications. This capability is ideal for computation-heavy tasks where JavaScript alone may struggle with efficiency. Rust, with its emphasis on speed, memory safety, and concurrency, is an excellent fit for Wasm, enabling performant web applications without sacrificing safety or reliability.

Some advantages of using Rust for Wasm projects include:

  • Memory Safety: Rust’s memory safety features prevent common issues like null pointers and data races, which are critical in concurrent or performance-focused environments.
  • High Performance: Rust's optimizations translate effectively to Wasm, enabling near-native speeds for complex calculations and tasks in web apps.
  • Robust Tooling: Rust's Wasm ecosystem includes tools like wasm-pack and wasm-bindgen, which simplify the workflow, making it easy to integrate Rust and Wasm into web applications.

Setting Up the wasm32-unknown-unknown Target

To build a Rust project for WebAssembly, we need the wasm32-unknown-unknown target, which compiles Rust code without the standard Rust runtime, allowing it to work in a JavaScript environment.

Step 1: Install the Required Tools

First, add the wasm32-unknown-unknown target to your Rust toolchain:

rustup target add wasm32-unknown-unknown

Next, install wasm-pack, a tool that simplifies the process of building, packaging, and publishing Rust code to WebAssembly:

cargo install wasm-pack

Step 2: Set Up the Rust Project

Let's create a new Rust library project named react-state-rs to build a state manager for React with Rust and Wasm:

cargo new --lib react-state-rs
cd react-state-rs

In Cargo.toml, add dependencies for wasm-bindgen, serde, serde_json, serde-wasm-bindgen, and ahash for faster HashMap operations:

[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.5"
ahash = "0.8"

Step 3: Implement the State Manager

Here’s a simple implementation of a state manager in Rust, designed for use in React. The manager uses AHash, a high-performance HashMap, to store state data with minimal overhead.

use std::sync::{Arc, RwLock};
use ahash::HashMap;
use serde_json::Value;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

pub type TState = HashMap<String, Value>;

#[wasm_bindgen]
pub struct State {
    initial_state: Arc<RwLock<TState>>,
    state: Arc<RwLock<TState>>,
}

#[wasm_bindgen]
impl State {
    #[wasm_bindgen(constructor)]
    pub fn new(state: JsValue) -> State {
        let initial_state = Arc::new(RwLock::new(
            serde_wasm_bindgen::from_value::<TState>(state.clone()).unwrap(),
        ));

        State {
            initial_state: Arc::clone(&initial_state),
            state: Arc::clone(&initial_state),
        }
    }

    pub fn get(&self) -> JsValue {
        let state = self.state.read().unwrap();
        serde_wasm_bindgen::to_value(&*state).unwrap()
    }

    pub fn get_initial(&self) -> JsValue {
        let initial_state = self.initial_state.read().unwrap();
        serde_wasm_bindgen::to_value(&*initial_state).unwrap()
    }

    pub fn set(&self, state: JsValue) {
        let _ = std::mem::replace(
            &mut *self.state.write().unwrap(),
            serde_wasm_bindgen::from_value::<TState>(state).unwrap(),
        );
    }

    pub fn clear(&self) {
        let _ = std::mem::replace(
            &mut *self.state.write().unwrap(),
            self.initial_state.read().unwrap().clone(),
        );
    }
}

Building and Generating WebAssembly Code

To generate the WebAssembly binary and JavaScript bindings, run:

wasm-pack build --target web --out-dir ../react/wasm

This command will create files in the specified output directory, including .wasm binaries and JavaScript bindings needed to interface with the Rust code in a web environment.

Folder Structure for Generated Files

After running the above command, you should see a directory structure similar to this:

react
├── wasm
│   ├── .gitignore
│   ├── package.json
│   ├── react_state_rs.d.ts
│   ├── react_state_rs.js
│   ├── react_state_rs_bg.wasm
│   ├── react_state_rs_bg.wasm.d.ts
├── .gitignore
├── .prettierrc
├── index.ts
├── package.json
├── tsconfig.json
├── yarn.lock
├── .gitattributes
└── README.md

The key files generated here are react_state_rs_bg.wasm, which contains the compiled WebAssembly binary, and react_state_rs.js, which provides JavaScript bindings to interact with the Wasm module.

Testing the State Manager with React and Comparing to Popular Libraries

To see how this state manager compares, let’s set up a simple benchmarking React app. We’ll use two popular state management libraries, Jotai and Zustand, to compare against our Wasm-based Rust state manager. Start by initializing a Vite project and adding dependencies:

npm create vite@latest
cd project-name
npm install jotai zustand dayjs

In App.tsx, add the following example code:

import dayjs from "dayjs";
import { atom, useAtom } from "jotai";
import { useEffect, useState } from "react";
import { create } from "react-state-rs";
import { create as zCreate } from "zustand";
import "./App.css";

const store = create<{ count: number }>({ count: 0 });
const zustandStore = zCreate<{ count: number }>(() => ({ count: 0 }));
const countAtom = atom(0);

function App() {
  const count = store((state) => state.count);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => store.setState({ count: count + 1 })}>Increment</button>
      <button onClick={() => store.setState({ count: count - 1 })}>Decrement</button>

      <Rust />
    </div>
  );
}

function Rust() {
  const count = store((state) => state.count);
  const [rustFinished, setRustFinished] = useState(false);

  useEffect(() => {
    const start = dayjs();

    for (let i = 0; i < 1000000; i++) {
      store.setState({ count: count + 1 });
    }

    console.log("Rust", dayjs().diff(start, "millisecond"));
    setRustFinished(true); 
  }, []);

  return (
    <div>
      <h1>{count}</h1>
      {rustFinished && <Zustand />}
    </div>
  );
}

function Zustand() {
  const zustandCount = zustandStore((state) => state.count);
  const [zustandFinished, setZustandFinished] = useState(false);

  useEffect(() => {
    const start = dayjs();

    for (let i = 0; i < 1000000; i++) {
      zustandStore.setState({ count: zustandCount + 1 });
    }

    console.log("Zustand", dayjs().diff(start, "millisecond"));
    setZustandFinished(true); 
  }, []);

  return (
    <div>
      <h1>{zustandCount}</h1>
      {zustandFinished && <Jotai />}
    </div>
  );
}

function Jotai() {
  const [count, setCount] = useAtom(countAtom);

  useEffect(() => {
    const start = dayjs();

    for (let i = 0; i < 1000000; i++) {
      setCount((c) => c + 1);
    }

    console.log("Jotai", dayjs().diff(start, "millisecond"));
  }, []);

  return (
    <div>
      <h1>{count}</h1>
    </div>
  );
}

export default App;

Performance Comparison

State ManagerExecution Time (1,000,000 operations)
Zustand330-340 ms
Jotai640-660 ms
Rust + Wasm1500-1600 ms

Our Wasm-based solution is slower due to the serialization/deserialization (ser/deser) overhead for each atomic operation. Unlike native JavaScript, where state changes are direct, Wasm requires extra steps to translate data between Rust and JavaScript, which

adds to the execution time.

Conclusion

This project is a proof of concept and a playground for experimenting with Rust and WebAssembly in state management. While our solution isn’t the fastest, it highlights the potential for Wasm in web development. Rust’s strengths in performance and safety can be valuable in web projects that require complex or intensive computations. The project is available on Github: react-state-rs.

For production, leveraging pure Rust frameworks that compile directly to Wasm, like Leptos and Dioxus, could provide even more efficient solutions. These frameworks allow entire web applications to be built in Rust, outperforming many JavaScript-based libraries and frameworks. As Wasm continues to evolve, the possibilities for Rust in web development are likely to expand, offering new ways to build performant and safe web applications.