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
andwasm-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 Manager | Execution Time (1,000,000 operations) |
---|---|
Zustand | 330-340 ms |
Jotai | 640-660 ms |
Rust + Wasm | 1500-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.