Expand description
§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
reactas 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.
$ cargo add wasm-react
$ cargo add wasm-bindgen@0.2§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:
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:
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.
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<JsValue, Error = JsValue>.
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<JsValue> for App {
type Error = JsValue;
fn try_from(_: JsValue) -> Result<Self, Self::Error> {
Ok(App)
}
}
export_components! { App }Use wasm-pack to compile your Rust code into WASM:
$ wasm-pack buildDepending on your JS project structure, you may want to specify the --target
option, see
wasm-pack documentation.
Assuming you use a bundler that supports JSX and WASM imports in ES modules like Webpack, you can use:
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(<App />);
}If you use plain ES modules, you can do the following:
$ wasm-pack build --target webimport "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:
// /.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!:
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:
use std::rc::Rc;
struct TaskList {
tasks: Vec<Rc<str>>
}You want to include TaskList in a container component App where tasks is
managed by a state:
use std::rc::Rc;
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::{use_state, State};
struct TaskList {
tasks: Vec<Rc<str>>
}
impl Component for TaskList {
fn render(&self) -> VNode {
/* … */
VNode::default()
}
}
struct App;
impl Component for App {
fn render(&self) -> VNode {
let tasks: State<Vec<Rc<str>>> = 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:
use std::rc::Rc;
use wasm_react::{h, Component, VNode};
use wasm_react::hooks::{use_state, State};
struct TaskList {
tasks: State<Vec<Rc<str>>>
}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<Vec<Rc<str>>> or
Memo<Vec<Rc<str>>> instead in the future or somewhere else. To be as generic
as possible, you can use PropContainer:
use std::rc::Rc;
use wasm_react::{h, Component, PropContainer, VNode};
use wasm_react::hooks::{use_state, State};
struct TaskList {
tasks: PropContainer<Vec<Rc<str>>>
}
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<Vec<Rc<str>>> = 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
StrictModecomponent.wasm-react uses React hooks to manually manage Rust memory.
StrictModewill 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 or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
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.
Modules§
- hooks
- This module contains bindings to React hooks.
- props
- This module provides convenience methods for building React props for JS consumption.
Macros§
- classnames
- Constructs a
Stringbased on various types that implementClassnames. - clones
- A helper macro which can be used to clone a list of variables. Helpful for creating a closure which clone-captures the environment.
- export_
components - This macro can be used to expose your
Componentfor JS consumption viawasm-bindgen. - h
- A convenience macro to
create_element()for creating HTML element nodes. - import_
components - This macro can be used to import JS React components for Rust consumption
via
wasm-bindgen.
Structs§
- Callback
- This is a simplified, reference-counted wrapper around an
FnMut(T) -> URust closure that may be called from JS whenTandUallow. - Context
- Represents a React context that can hold a global state.
- Context
Provider - A component that can make the given context available for its subtrees.
- Keyed
- Wraps your component to assign a React key to it.
- Memoized
- Wraps your component to let React skip rendering if props haven’t changed.
- Suspense
- A component that specifies the loading indicator when loading lazy descendant components.
- Void
- A zero-sized helper struct to simulate a JS-interoperable
Callbackwith no input arguments.
Enums§
- Prop
Container - A helpful abstraction over non-
Copytypes that can be used as a prop type for components. - Prop
Container Ref - Allows read-only access to the underlying value of
PropContainer. - VNode
- Represents a node in the virtual DOM of React.
Traits§
- Component
- Implement this trait on a struct to create a component with the struct as props.
- KeyType
- Implemented by types which can serve as a React key.
Functions§
- create_
context - Creates a new React context that can hold a global state.
- create_
element - The Rust equivalent to
React.createElement. Useh!for a more convenient way to create HTML element nodes. To create Rust components, useComponent::build().