Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/yishn/wasm-react

WASM bindings for React.
https://github.com/yishn/wasm-react

javascript js react rust rust-wasm ui wasm wasm-bindgen webassembly

Last synced: 4 days ago
JSON representation

WASM bindings for React.

Awesome Lists containing this project

README

        

# wasm-react 🦀⚛️

[![GitHub](https://img.shields.io/badge/GitHub-Repo-lightgrey?logo=github)](https://github.com/yishn/wasm-react)
[![crates.io](https://img.shields.io/crates/v/wasm-react)](https://crates.io/crates/wasm-react)
[![CI](https://github.com/yishn/wasm-react/actions/workflows/ci.yml/badge.svg)](https://github.com/yishn/wasm-react/actions/workflows/ci.yml)
[![docs.rs](https://img.shields.io/docsrs/wasm-react)](https://docs.rs/wasm-react/)

WASM bindings for [React].

## Introduction

This library enables you to write and use React components in Rust, which then
can be exported to JS to be reused or rendered.

### Why React?

React is one of the most popular UI framework for JS with a thriving community
and lots of libraries written for it. Standing on the shoulder of giants, you
will be able to write complex frontend applications with Rust.

### Goals

- Provide Rust bindings for the public API of `react` as close to the original
API as possible, but with Rust in mind.
- Provide an ergonomic way to write components.
- Provide ways to interact with components written in JS.

### Non-Goals

- Provide bindings for any other library than `react`, e.g. `react-dom`.
- Reimplementation of the reconciliation algorithm or runtime.
- Emphasis on performance.

## Getting Started

Make sure you have Rust and Cargo installed. You can install `wasm-react` with
cargo. Furthermore, if you want to expose your Rust components to JS, you also
need `wasm-bindgen` and have [`wasm-pack`] installed.

```sh
$ cargo add wasm-react
$ cargo add [email protected]
```

### Creating a Component

First, you need to define a struct for the props of your component. To define
the render function, you need to implement the trait `Component` for your
struct:

```rust
use wasm_react::{h, Component, VNode};

struct Counter {
counter: i32,
}

impl Component for Counter {
fn render(&self) -> VNode {
h!(div)
.build((
h!(p).build(("Counter: ", self.counter)),
h!(button).build("Increment"),
))
}
}
```

### Add State

You can use the `use_state()` hook to make your component stateful:

```rust
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::use_state;

struct Counter {
initial_counter: i32,
}

impl Component for Counter {
fn render(&self) -> VNode {
let counter = use_state(|| self.initial_counter);

let result = h!(div)
.build((
h!(p).build(("Counter: ", *counter.value())),
h!(button).build("Increment"),
));
result
}
}
```

Note that according to the usual Rust rules, the state will be dropped when the
render function returns. `use_state()` will prevent that by tying the lifetime
of the state to the lifetime of the component, therefore _persisting_ the state
through the entire lifetime of the component.

### Add Event Handlers

To create an event handler, you pass a `Callback` created from a Rust closure.
You can use the helper macro `clones!` to clone-capture the environment more
ergonomically.

```rust
use wasm_react::{h, clones, Component, Callback, VNode};
use wasm_react::hooks::{use_state, Deps};

struct Counter {
initial_counter: i32,
}

impl Component for Counter {
fn render(&self) -> VNode {
let message = use_state(|| "Hello World!");
let counter = use_state(|| self.initial_counter);

let result = h!(div)
.build((
h!(p).build(("Counter: ", *counter.value())),

h!(button)
.on_click(&Callback::new({
clones!(message, mut counter);

move |_| {
println!("{}", message.value());
counter.set(|c| c + 1);
}
}))
.build("Increment"),

h!(button)
.on_click(&Callback::new({
clones!(mut counter);

move |_| counter.set(|c| c - 1)
}))
.build("Decrement"),
));
result
}
}
```

### Export Components for JS Consumption

First, you'll need [`wasm-pack`]. You can use `export_components!` to export
your Rust component for JS consumption. Requirement is that your component
implements `TryFrom`.

```rust
use wasm_react::{h, export_components, Component, VNode};
use wasm_bindgen::JsValue;

struct Counter {
initial_counter: i32,
}

impl Component for Counter {
fn render(&self) -> VNode {
/* … */
VNode::new()
}
}

struct App;

impl Component for App {
fn render(&self) -> VNode {
h!(div).build((
Counter {
initial_counter: 0,
}
.build(),
))
}
}

impl TryFrom for App {
type Error = JsValue;

fn try_from(_: JsValue) -> Result {
Ok(App)
}
}

export_components! { App }
```

Use `wasm-pack` to compile your Rust code into WASM:

```sh
$ wasm-pack build
```

Depending on your JS project structure, you may want to specify the `--target`
option, see
[`wasm-pack` documentation](https://rustwasm.github.io/docs/wasm-pack/commands/build.html#target).

Assuming you use a bundler that supports JSX and WASM imports in ES modules like
Webpack, you can use:

```js
import React from "react";
import { createRoot } from "react-dom/client";

async function main() {
const { WasmReact, App } = await import("./path/to/pkg/project.js");
WasmReact.useReact(React); // Tell wasm-react to use your React runtime

const root = createRoot(document.getElementById("root"));
root.render();
}
```

If you use plain ES modules, you can do the following:

```sh
$ wasm-pack build --target web
```

```js
import "https://unpkg.com/react/umd/react.production.min.js";
import "https://unpkg.com/react-dom/umd/react-dom.production.min.js";
import init, { WasmReact, App } from "./path/to/pkg/project.js";

async function main() {
await init(); // Need to load WASM first
WasmReact.useReact(window.React); // Tell wasm-react to use your React runtime

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(React.createElement(App, {}));
}
```

### Import Components for Rust Consumption

You can use `import_components!` together with `wasm-bindgen` to import JS
components for Rust consumption. First, prepare your JS component:

```js
// /.dummy/myComponents.js
import "https://unpkg.com/react/umd/react.production.min.js";

export function MyComponent(props) {
/* … */
}
```

Make sure the component uses the same React runtime as specified for
`wasm-react`. Afterwards, use `import_components!`:

```rust
use wasm_react::{h, import_components, Component, VNode};
use wasm_bindgen::prelude::*;

import_components! {
#[wasm_bindgen(module = "/.dummy/myComponents.js")]

MyComponent
}

struct App;

impl Component for App {
fn render(&self) -> VNode {
h!(div).build((
MyComponent::new()
.attr("prop", &"Hello World!".into())
.build(()),
))
}
}
```

### Passing Down Non-Copy Props

Say you define a component with the following struct:

```rust
use std::rc::Rc;

struct TaskList {
tasks: Vec>
}
```

You want to include `TaskList` in a container component `App` where `tasks` is
managed by a state:

```rust
use std::rc::Rc;
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::{use_state, State};

struct TaskList {
tasks: Vec>
}

impl Component for TaskList {
fn render(&self) -> VNode {
/* … */
VNode::default()
}
}

struct App;

impl Component for App {
fn render(&self) -> VNode {
let tasks: State>> = use_state(|| vec![]);

h!(div).build((
TaskList {
tasks: todo!(), // Oops, `tasks.value()` does not fit the type
}
.build(),
))
}
}
```

Changing the type of `tasks` to fit `tasks.value()` doesn't work, since
`tasks.value()` returns a non-`'static` reference while component structs can
only contain `'static` values. You can clone the underlying `Vec`, but this
introduces unnecessary overhead. In this situation you might think you can
simply change the type of `TaskList` to a `State`:

```rust
use std::rc::Rc;
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::{use_state, State};

struct TaskList {
tasks: State>>
}
```

This works as long as the prop `tasks` is guaranteed to come from a state. But
this assumption may not hold. You might want to pass on `Rc>>` or
`Memo>>` instead in the future or somewhere else. To be as generic
as possible, you can use `PropContainer`:

```rust
use std::rc::Rc;
use wasm_react::{h, Component, PropContainer, VNode};
use wasm_react::hooks::{use_state, State};

struct TaskList {
tasks: PropContainer>>
}

impl Component for TaskList {
fn render(&self) -> VNode {
/* Do something with `self.tasks.value()`… */
VNode::default()
}
}

struct App;

impl Component for App {
fn render(&self) -> VNode {
let tasks: State>> = use_state(|| vec![]);

h!(div).build((
TaskList {
// Cloning `State` has low cost as opposed to cloning the underlying
// `Vec`.
tasks: tasks.clone().into(),
}
.build(),
))
}
}
```

## Known Caveats

- Rust components cannot be part of the subtree of a `StrictMode` component.

wasm-react uses React hooks to manually manage Rust memory. `StrictMode` will
run hooks and their destructors twice which will result in a double free.

## License

Licensed under either of

- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
)

at your option.

## Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.

[react]: https://react.dev
[`wasm-pack`]: https://rustwasm.github.io/wasm-pack/