├── .dummy └── myComponents.js ├── rustfmt.toml ├── .gitignore ├── src ├── hooks │ ├── use_id.rs │ ├── use_callback.rs │ ├── mod.rs │ ├── use_tmp_ref.rs │ ├── use_context.rs │ ├── use_deferred_value.rs │ ├── deps.rs │ ├── use_memo.rs │ ├── use_transition.rs │ ├── use_state.rs │ ├── use_js_ref.rs │ ├── use_effect.rs │ └── use_ref.rs ├── props │ ├── mod.rs │ ├── classnames.rs │ ├── h.rs │ ├── props.rs │ ├── h_attrs.rs │ ├── h_events.rs │ └── style.rs ├── builtin_components.rs ├── lib.rs ├── react_bindings │ ├── mod.rs │ └── react-bindings.js ├── prop_container.rs ├── vnode.rs ├── context.rs ├── callback.rs ├── component.rs └── macros.rs ├── package.json ├── examples ├── 03-material-ui │ ├── webpack.config.js │ ├── README.md │ ├── js │ │ ├── main.js │ │ └── mui-components.js │ ├── Cargo.toml │ ├── index.html │ ├── package.json │ └── src │ │ └── lib.rs ├── 02-todo │ ├── README.md │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── lib.rs ├── 04-context │ ├── README.md │ ├── Cargo.toml │ ├── index.html │ └── src │ │ ├── card.rs │ │ ├── button.rs │ │ └── lib.rs └── 01-hello-world │ ├── README.md │ ├── Cargo.toml │ ├── src │ └── lib.rs │ └── index.html ├── .github └── workflows │ └── ci.yml ├── ci └── build-examples.js ├── Cargo.toml ├── LICENSE-MIT ├── README.md └── LICENSE-APACHE /.dummy/myComponents.js: -------------------------------------------------------------------------------- 1 | // dummy file for tests 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | max_width = 80 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | package-lock.json 4 | 5 | target 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /src/hooks/use_id.rs: -------------------------------------------------------------------------------- 1 | use crate::react_bindings; 2 | 3 | /// Returns a unique component ID which is stable across server and client. 4 | pub fn use_id() -> String { 5 | react_bindings::use_id() 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "start": "serve", 5 | "build-examples": "node ./ci/build-examples.js" 6 | }, 7 | "devDependencies": { 8 | "serve": "^14.2.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/03-material-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | 3 | module.exports = { 4 | entry: "./js/main.js", 5 | output: { 6 | path: resolve(__dirname, "dist"), 7 | filename: "bundle.js", 8 | }, 9 | experiments: { 10 | asyncWebAssembly: true, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/03-material-ui/README.md: -------------------------------------------------------------------------------- 1 | # Material UI 2 | 3 | Make sure you have Node.js, npm, and [wasm-pack] installed. To run this example, 4 | execute the following commands: 5 | 6 | ```sh 7 | $ npm install 8 | $ npm run build 9 | $ npm start 10 | ``` 11 | 12 | [wasm-pack]: https://rustwasm.github.io/wasm-pack/ 13 | -------------------------------------------------------------------------------- /examples/02-todo/README.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | Make sure you have [wasm-pack] and [sfz] installed. To run this example, execute 4 | the following commands: 5 | 6 | ```sh 7 | $ wasm-pack build --target web 8 | $ sfz 9 | ``` 10 | 11 | [wasm-pack]: https://rustwasm.github.io/wasm-pack/ 12 | [sfz]: https://crates.io/crates/sfz 13 | -------------------------------------------------------------------------------- /examples/04-context/README.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | Make sure you have [wasm-pack] and [sfz] installed. To run this example, execute 4 | the following commands: 5 | 6 | ```sh 7 | $ wasm-pack build --target web 8 | $ sfz 9 | ``` 10 | 11 | [wasm-pack]: https://rustwasm.github.io/wasm-pack/ 12 | [sfz]: https://crates.io/crates/sfz 13 | -------------------------------------------------------------------------------- /examples/01-hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World! 2 | 3 | Make sure you have [wasm-pack] and [sfz] installed. To run this example, execute 4 | the following commands: 5 | 6 | ```sh 7 | $ wasm-pack build --target web 8 | $ sfz 9 | ``` 10 | 11 | [wasm-pack]: https://rustwasm.github.io/wasm-pack/ 12 | [sfz]: https://crates.io/crates/sfz 13 | -------------------------------------------------------------------------------- /src/props/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module provides convenience methods for building React props for 2 | //! JS consumption. 3 | 4 | mod h_attrs; 5 | mod classnames; 6 | mod h_events; 7 | mod props; 8 | mod h; 9 | mod style; 10 | 11 | pub use h::*; 12 | pub use h_attrs::*; 13 | pub use classnames::*; 14 | pub use h_events::*; 15 | pub use props::*; 16 | pub use style::*; 17 | -------------------------------------------------------------------------------- /examples/04-context/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "context" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | wasm-react = { path = "../.." } 13 | wasm-bindgen = "0.2" 14 | js-sys = "0.3" 15 | -------------------------------------------------------------------------------- /examples/01-hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | wasm-react = { path = "../.." } 13 | wasm-bindgen = "0.2" 14 | js-sys = "0.3" 15 | -------------------------------------------------------------------------------- /examples/03-material-ui/js/main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | async function main() { 5 | const { WasmReact, App } = await import("../pkg/material_ui.js"); 6 | WasmReact.useReact(React); 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render(React.createElement(App)); 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /examples/03-material-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "material-ui" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | wasm-react = { path = "../.." } 13 | wasm-bindgen = "0.2" 14 | js-sys = "0.3" 15 | web-sys = "0.3" 16 | -------------------------------------------------------------------------------- /src/hooks/use_callback.rs: -------------------------------------------------------------------------------- 1 | use super::{use_memo, Deps}; 2 | use crate::Callback; 3 | 4 | /// Returns a memoized callback. 5 | pub fn use_callback(f: Callback, deps: Deps) -> Callback 6 | where 7 | T: 'static, 8 | U: 'static, 9 | D: PartialEq + 'static, 10 | { 11 | let memo = use_memo(move || f, deps); 12 | let result = memo.value().clone(); 13 | 14 | result 15 | } 16 | -------------------------------------------------------------------------------- /examples/02-todo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | wasm-react = { path = "../.." } 13 | wasm-bindgen = "0.2" 14 | js-sys = "0.3" 15 | web-sys = { version = "0.3", features = ["HtmlInputElement"] } 16 | -------------------------------------------------------------------------------- /examples/03-material-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example 7 | 8 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/hooks/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains bindings to React hooks. 2 | 3 | mod deps; 4 | mod use_callback; 5 | mod use_context; 6 | mod use_deferred_value; 7 | mod use_effect; 8 | mod use_id; 9 | mod use_js_ref; 10 | mod use_memo; 11 | mod use_ref; 12 | mod use_state; 13 | mod use_tmp_ref; 14 | mod use_transition; 15 | 16 | pub use deps::*; 17 | pub use use_callback::*; 18 | pub use use_context::*; 19 | pub use use_deferred_value::*; 20 | pub use use_effect::*; 21 | pub use use_id::*; 22 | pub use use_js_ref::*; 23 | pub use use_memo::*; 24 | pub use use_ref::*; 25 | pub use use_state::*; 26 | pub(crate) use use_tmp_ref::*; 27 | pub use use_transition::*; 28 | -------------------------------------------------------------------------------- /examples/03-material-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "start": "serve", 6 | "build": "rimraf ./dist && wasm-pack build --target bundler && webpack --config ./webpack.config.js --mode development" 7 | }, 8 | "dependencies": { 9 | "@emotion/react": "^11.9.0", 10 | "@emotion/styled": "^11.8.1", 11 | "@mui/icons-material": "^5.8.3", 12 | "@mui/material": "^5.8.3", 13 | "react": "^18.1.0", 14 | "react-dom": "^18.1.0" 15 | }, 16 | "devDependencies": { 17 | "rimraf": "^3.0.2", 18 | "serve": "^13.0.2", 19 | "webpack": "^5.73.0", 20 | "webpack-cli": "^4.9.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: jetli/wasm-pack-action@v0.3.0 19 | with: 20 | version: latest 21 | - name: Build 22 | run: | 23 | wasm-pack build --debug 24 | - name: Build doc 25 | run: | 26 | cargo doc 27 | - name: Build examples 28 | run: | 29 | npm run build-examples 30 | - name: Run tests 31 | run: | 32 | cargo test --verbose 33 | -------------------------------------------------------------------------------- /src/hooks/use_tmp_ref.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use wasm_bindgen::{prelude::wasm_bindgen, UnwrapThrowExt}; 3 | 4 | use crate::react_bindings; 5 | 6 | #[doc(hidden)] 7 | #[wasm_bindgen(js_name = __WasmReact_TmpRef)] 8 | pub struct TmpRef(Box); 9 | 10 | /// Temporarily persists a value. 11 | /// 12 | /// The value will live until the next rerender. Callback functions will be 13 | /// persisted this way. 14 | pub(crate) fn use_tmp_ref(value: T, mut callback: impl FnMut(&T)) 15 | where 16 | T: 'static, 17 | { 18 | react_bindings::use_rust_tmp_ref( 19 | TmpRef(Box::new(value)), 20 | &mut |tmp_ref| callback(tmp_ref.0.downcast_ref().unwrap_throw()), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/03-material-ui/js/mui-components.js: -------------------------------------------------------------------------------- 1 | export { default as Box } from "@mui/material/Box"; 2 | export { default as Container } from "@mui/material/Container"; 3 | export { default as AppBar } from "@mui/material/AppBar"; 4 | export { default as Toolbar } from "@mui/material/Toolbar"; 5 | export { default as Typography } from "@mui/material/Typography"; 6 | export { default as Card } from "@mui/material/Card"; 7 | export { default as CardContent } from "@mui/material/CardContent"; 8 | export { default as CardActions } from "@mui/material/CardActions"; 9 | export { default as IconButton } from "@mui/material/IconButton"; 10 | export { default as Button } from "@mui/material/Button"; 11 | export { default as MenuIcon } from "@mui/icons-material/Menu"; 12 | -------------------------------------------------------------------------------- /src/hooks/use_context.rs: -------------------------------------------------------------------------------- 1 | use std::{rc::Rc, thread::LocalKey}; 2 | use wasm_bindgen::UnwrapThrowExt; 3 | 4 | use crate::{react_bindings, Context}; 5 | 6 | /// Allows access to the current context value of the given context. 7 | /// 8 | /// See [`create_context()`](crate::create_context()) for usage. 9 | pub fn use_context(context: &'static LocalKey>) -> Rc { 10 | let mut result = None; 11 | 12 | context.with(|context| { 13 | react_bindings::use_rust_context( 14 | context.as_ref(), 15 | &mut |ref_container_value| { 16 | result = Some( 17 | ref_container_value 18 | .value::() 19 | .expect_throw("mismatched context type"), 20 | ); 21 | }, 22 | ); 23 | }); 24 | 25 | result.expect_throw("callback was not called") 26 | } 27 | -------------------------------------------------------------------------------- /examples/01-hello-world/src/lib.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Reflect; 2 | use std::rc::Rc; 3 | use wasm_bindgen::prelude::*; 4 | use wasm_react::{export_components, h, Component, VNode}; 5 | 6 | pub struct App { 7 | name: Option>, 8 | } 9 | 10 | impl TryFrom for App { 11 | type Error = JsValue; 12 | 13 | fn try_from(value: JsValue) -> Result { 14 | Ok(App { 15 | name: Reflect::get(&value, &"name".into())? 16 | .as_string() 17 | .map(|x| x.into()), 18 | }) 19 | } 20 | } 21 | 22 | impl Component for App { 23 | fn render(&self) -> VNode { 24 | h!(h1).build( 25 | // 26 | if let Some(name) = self.name.as_ref() { 27 | format!("Hello {name}!") 28 | } else { 29 | "Hello World!".to_string() 30 | }, 31 | ) 32 | } 33 | } 34 | 35 | export_components! { App } 36 | -------------------------------------------------------------------------------- /src/props/classnames.rs: -------------------------------------------------------------------------------- 1 | /// A trait for types to be used in [`classnames!`](crate::classnames!). 2 | pub trait Classnames { 3 | /// Appends the class to a string. 4 | fn append_to(&self, string: &mut String); 5 | } 6 | 7 | impl Classnames for &str { 8 | fn append_to(&self, string: &mut String) { 9 | string.push_str(self); 10 | string.push(' '); 11 | } 12 | } 13 | 14 | impl Classnames for String { 15 | fn append_to(&self, string: &mut String) { 16 | (&self[..]).append_to(string); 17 | } 18 | } 19 | 20 | impl Classnames for &String { 21 | fn append_to(&self, string: &mut String) { 22 | (&self[..]).append_to(string); 23 | } 24 | } 25 | 26 | impl Classnames for Option { 27 | fn append_to(&self, string: &mut String) { 28 | if let Some(value) = self { 29 | value.append_to(string); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/02-todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example 7 | 8 | 9 | 10 |
11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/04-context/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example 7 | 8 | 9 | 10 |
11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ci/build-examples.js: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs/promises"; 2 | import * as path from "node:path"; 3 | import { execSync } from "node:child_process"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | const examplesPath = path.resolve(__dirname, "../examples"); 8 | const items = await fs.readdir(examplesPath); 9 | 10 | const exists = (p) => 11 | fs.access(p).then( 12 | () => true, 13 | () => false 14 | ); 15 | 16 | for (const item of items) { 17 | const examplePath = path.resolve(examplesPath, item); 18 | const exec = (command) => 19 | execSync(command, { cwd: examplePath, stdio: "inherit" }); 20 | 21 | if (await exists(path.resolve(examplePath, "Cargo.toml"))) { 22 | exec("wasm-pack build --target web"); 23 | } 24 | 25 | if (await exists(path.resolve(examplePath, "package.json"))) { 26 | exec("npm install"); 27 | exec("npm run build"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/01-hello-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Example 7 | 8 | 9 | 10 |
11 | 12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/04-context/src/card.rs: -------------------------------------------------------------------------------- 1 | use crate::{Theme, THEME_CONTEXT}; 2 | use wasm_react::{h, hooks::use_context, props::Style, Component, VNode}; 3 | 4 | #[derive(Debug, Default)] 5 | pub struct Card { 6 | children: VNode, 7 | } 8 | 9 | impl Card { 10 | pub fn new() -> Self { 11 | Self::default() 12 | } 13 | 14 | pub fn build(mut self, children: impl Into) -> VNode { 15 | self.children = children.into(); 16 | Component::build(self) 17 | } 18 | } 19 | 20 | impl Component for Card { 21 | fn render(&self) -> VNode { 22 | let theme = use_context(&THEME_CONTEXT); 23 | let style = { 24 | let mut style = 25 | Style::new().padding("1px 10px").border("1px solid black"); 26 | 27 | if let Theme::DarkMode = *theme { 28 | style = style 29 | .background_color("#333") 30 | .color("#eee") 31 | .border_color("#ccc"); 32 | } 33 | 34 | style 35 | }; 36 | 37 | h!(div[."card"]).style(&style).build(self.children.clone()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/04-context/src/button.rs: -------------------------------------------------------------------------------- 1 | use crate::{Theme, THEME_CONTEXT}; 2 | use std::rc::Rc; 3 | use wasm_react::{h, hooks::use_context, props::Style, Component, VNode}; 4 | 5 | #[derive(Debug)] 6 | pub struct Button { 7 | text: Rc, 8 | } 9 | 10 | impl Button { 11 | pub fn new() -> Self { 12 | Self { 13 | text: Rc::from("Button"), 14 | } 15 | } 16 | 17 | pub fn build(mut self, text: &str) -> VNode { 18 | self.text = Rc::from(text); 19 | Component::build(self) 20 | } 21 | } 22 | 23 | impl Component for Button { 24 | fn render(&self) -> VNode { 25 | let theme = use_context(&THEME_CONTEXT); 26 | let style = { 27 | let mut style = 28 | Style::new().padding("5px 10px").border("1px solid black"); 29 | 30 | if let Theme::DarkMode = *theme { 31 | style = style 32 | .background_color("#444") 33 | .color("#eee") 34 | .border_color("#ccc"); 35 | } 36 | 37 | style 38 | }; 39 | 40 | h!(button).style(&style).build(&*self.text) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-react" 3 | version = "0.6.0" 4 | edition = "2021" 5 | authors = ["Yichuan Shen"] 6 | description = "WASM bindings for React." 7 | repository = "https://github.com/yishn/wasm-react" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["react", "ui", "wasm", "js", "web"] 10 | categories = ["gui", "wasm", "web-programming"] 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [profile.release] 18 | lto = true 19 | opt-level = 's' 20 | 21 | [dependencies] 22 | wasm-bindgen = "0.2.87" 23 | js-sys = "0.3.64" 24 | paste = "1.0.14" 25 | 26 | [dependencies.web-sys] 27 | version = "0.3.64" 28 | features = [ 29 | "Event", "MouseEvent", "FocusEvent", "KeyboardEvent", "DragEvent", 30 | "PointerEvent", "WheelEvent", "AnimationEvent", "TransitionEvent", 31 | "Element" 32 | ] 33 | 34 | [workspace] 35 | members = [ 36 | "./examples/01-hello-world", 37 | "./examples/02-todo", 38 | "./examples/03-material-ui", 39 | "./examples/04-context", 40 | ] 41 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yichuan Shen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/hooks/use_deferred_value.rs: -------------------------------------------------------------------------------- 1 | use super::{use_ref, RefContainer}; 2 | use crate::react_bindings; 3 | use std::cell::Ref; 4 | use wasm_bindgen::UnwrapThrowExt; 5 | 6 | /// Allows access to the underlying deferred value persisted with 7 | /// [`use_deferred_value()`]. 8 | #[derive(Debug)] 9 | pub struct DeferredValue(RefContainer>); 10 | 11 | impl DeferredValue { 12 | /// Returns a reference to the underlying deferred value. 13 | pub fn value(&self) -> Ref<'_, T> { 14 | Ref::map(self.0.current(), |x| { 15 | &x.as_ref().expect_throw("no deferred value available").0 16 | }) 17 | } 18 | } 19 | 20 | impl Clone for DeferredValue { 21 | fn clone(&self) -> Self { 22 | Self(self.0.clone()) 23 | } 24 | } 25 | 26 | /// Returns the given value, or in case of urgent updates, returns the previous 27 | /// value given. 28 | pub fn use_deferred_value(value: T) -> DeferredValue { 29 | let mut ref_container = use_ref(None::<(T, u8)>); 30 | 31 | let deferred_counter = react_bindings::use_deferred_value( 32 | ref_container 33 | .current() 34 | .as_ref() 35 | .map(|current| current.1.wrapping_add(1)) 36 | .unwrap_or(0), 37 | ); 38 | 39 | if Some(deferred_counter) 40 | != ref_container.current().as_ref().map(|current| current.1) 41 | { 42 | // Deferred value changed 43 | ref_container.set_current(Some((value, deferred_counter))); 44 | } 45 | 46 | DeferredValue(ref_container) 47 | } 48 | -------------------------------------------------------------------------------- /src/builtin_components.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | props::{HType, H}, 3 | react_bindings, VNode, 4 | }; 5 | use std::borrow::Cow; 6 | use wasm_bindgen::JsValue; 7 | 8 | /// A component that specifies the loading indicator when loading lazy descendant 9 | /// components. 10 | /// 11 | /// For more information, see [React documentation about code-splitting][docs]. 12 | /// 13 | /// [docs]: https://react.dev/reference/react/Suspense 14 | /// 15 | /// # Example 16 | /// 17 | /// ``` 18 | /// # use wasm_react::*; 19 | /// # 20 | /// # struct SomeLazyComponent {} 21 | /// # impl Component for SomeLazyComponent { 22 | /// # fn render(&self) -> VNode { VNode::default() } 23 | /// # } 24 | /// # 25 | /// # fn f() -> VNode { 26 | /// Suspense::new() 27 | /// .fallback( 28 | /// h!(div[."loading"]).build("Loading…"), 29 | /// ) 30 | /// .build( 31 | /// SomeLazyComponent { /* … */ }.build() 32 | /// ) 33 | /// # } 34 | /// ``` 35 | #[derive(Debug, Default, Clone, Copy)] 36 | pub struct Suspense; 37 | 38 | impl HType for Suspense { 39 | fn as_js(&self) -> Cow<'_, JsValue> { 40 | Cow::Borrowed(&react_bindings::SUSPENSE) 41 | } 42 | } 43 | 44 | impl Suspense { 45 | /// Creates a new `React.Suspense` component builder. 46 | pub fn new() -> H { 47 | H::new(Suspense) 48 | } 49 | } 50 | 51 | impl H { 52 | /// Sets a fallback when loading lazy descendant components. 53 | pub fn fallback(self, children: impl Into) -> Self { 54 | self.attr("fallback", children.into().as_ref()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/hooks/deps.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | /// This struct specifies dependencies for certain hooks. 4 | /// 5 | /// # Example 6 | /// 7 | /// ``` 8 | /// # use wasm_react::{*, hooks::*}; 9 | /// # fn log(s: &str) {} 10 | /// # struct State { counter: () } 11 | /// # struct F { id: () }; 12 | /// # impl F { 13 | /// # fn f(&self, state: State) { 14 | /// # 15 | /// use_effect(|| { 16 | /// log("This effect will be called every time `self.id` or `state.counter` changes."); 17 | /// }, Deps::some((self.id, state.counter))); 18 | /// # 19 | /// # } 20 | /// # } 21 | /// ``` 22 | #[derive(PartialEq, Clone, Copy)] 23 | pub struct Deps(Option); 24 | 25 | impl Debug for Deps { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | let mut result = f.debug_tuple("Deps"); 28 | 29 | match self.0.as_ref() { 30 | Some(deps) => result.field(&deps), 31 | None => result.field(&"All"), 32 | } 33 | .finish() 34 | } 35 | } 36 | 37 | impl Deps<()> { 38 | /// The hook will be activated whenever the component renders. 39 | pub fn all() -> Self { 40 | Self(None) 41 | } 42 | 43 | /// The hook will be activated only on the first render. 44 | pub fn none() -> Self { 45 | Self(Some(())) 46 | } 47 | } 48 | 49 | impl Deps { 50 | /// The hook will be activated every time when the component renders if the 51 | /// inner value `T` has changed from last render. 52 | pub fn some(deps: T) -> Self { 53 | Self(Some(deps)) 54 | } 55 | 56 | pub(crate) fn is_all(&self) -> bool { 57 | self.0.is_none() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | // This hack is needed to let the doctests run for our README file 5 | #[cfg(doctest)] 6 | #[doc = include_str!("../README.md")] 7 | extern "C" {} 8 | 9 | mod builtin_components; 10 | mod callback; 11 | mod component; 12 | mod context; 13 | mod macros; 14 | mod prop_container; 15 | mod vnode; 16 | 17 | pub mod hooks; 18 | pub mod props; 19 | #[doc(hidden)] 20 | pub mod react_bindings; 21 | 22 | use props::Props; 23 | use wasm_bindgen::prelude::*; 24 | 25 | pub use builtin_components::*; 26 | pub use callback::*; 27 | pub use component::*; 28 | pub use context::*; 29 | #[doc(hidden)] 30 | pub use paste::paste; 31 | pub use prop_container::*; 32 | pub use vnode::*; 33 | 34 | /// Contains all functions exported to JS by `wasm-react`. These functions should 35 | /// be called from JS only. 36 | #[doc(hidden)] 37 | #[wasm_bindgen] 38 | pub struct WasmReact; 39 | 40 | #[wasm_bindgen] 41 | impl WasmReact { 42 | /// Set the React runtime that `wasm-react` should use. 43 | /// 44 | /// Calling this function multiple times will result in no-ops. 45 | /// 46 | /// # Example 47 | /// 48 | /// ```js 49 | /// import React from "react"; 50 | /// import init, { WasmReact } from "./path/to/pkg/project.js"; 51 | /// 52 | /// async function main() { 53 | /// await init(); 54 | /// WasmReact.useReact(React); 55 | /// } 56 | /// 57 | /// main(); 58 | /// ``` 59 | #[wasm_bindgen(js_name = useReact)] 60 | pub fn use_react(value: &JsValue) { 61 | react_bindings::use_react(value); 62 | } 63 | } 64 | 65 | /// The Rust equivalent to `React.createElement`. Use [`h!`] for a more 66 | /// convenient way to create HTML element nodes. To create Rust components, use 67 | /// [`Component::build()`]. 68 | pub fn create_element(typ: &JsValue, props: &Props, children: VNode) -> VNode { 69 | VNode::Single(react_bindings::create_element( 70 | typ, 71 | props.as_ref(), 72 | &children.into(), 73 | )) 74 | } 75 | -------------------------------------------------------------------------------- /src/hooks/use_memo.rs: -------------------------------------------------------------------------------- 1 | use super::{use_ref, Deps, RefContainer}; 2 | use std::cell::Ref; 3 | use wasm_bindgen::UnwrapThrowExt; 4 | 5 | /// Allows access to the underlying memoized data persisted with [`use_memo()`]. 6 | #[derive(Debug)] 7 | pub struct Memo(RefContainer>); 8 | 9 | impl Memo { 10 | /// Returns a reference to the underlying memoized data. 11 | pub fn value(&self) -> Ref<'_, T> { 12 | Ref::map(self.0.current(), |x| { 13 | x.as_ref().expect_throw("no memo data available") 14 | }) 15 | } 16 | } 17 | 18 | impl Clone for Memo { 19 | fn clone(&self) -> Self { 20 | Self(self.0.clone()) 21 | } 22 | } 23 | 24 | /// Returns a persisted, memoized value. 25 | /// 26 | /// This will recompute the value with the given closure whenever the given 27 | /// dependencies has changed from last render. This optimization helps to avoid 28 | /// expensive calculations on every render. 29 | /// 30 | /// # Example 31 | /// 32 | /// ``` 33 | /// # use wasm_react::{*, hooks::*}; 34 | /// # 35 | /// # fn compute_expensive_value(a: (), b: ()) -> &'static str { "" } 36 | /// # struct C { a: (), b:() }; 37 | /// # impl C { 38 | /// fn render(&self) -> VNode { 39 | /// let a = self.a; 40 | /// let b = self.b; 41 | /// let memo = use_memo(|| compute_expensive_value(a, b), Deps::some((a, b))); 42 | /// 43 | /// let vnode = h!(div).build(*memo.value()); 44 | /// vnode 45 | /// } 46 | /// # } 47 | /// ``` 48 | pub fn use_memo(create: impl FnOnce() -> T, deps: Deps) -> Memo 49 | where 50 | T: 'static, 51 | D: PartialEq + 'static, 52 | { 53 | let mut deps_ref_container = use_ref(None::>); 54 | let mut value_ref_container = use_ref(None::); 55 | 56 | let need_update = { 57 | let current = deps_ref_container.current(); 58 | let old_deps = current.as_ref(); 59 | 60 | deps.is_all() || Some(&deps) != old_deps 61 | }; 62 | 63 | if need_update { 64 | deps_ref_container.set_current(Some(deps)); 65 | value_ref_container.set_current(Some(create())); 66 | } 67 | 68 | Memo(value_ref_container) 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/use_transition.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Function; 2 | use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt}; 3 | 4 | use crate::react_bindings; 5 | 6 | /// Allows access to the transition state. 7 | #[derive(Debug, Clone)] 8 | pub struct Transition { 9 | is_pending: bool, 10 | start_transition: Function, 11 | } 12 | 13 | impl Transition { 14 | /// Returns whether the transition is active or not. 15 | pub fn is_pending(&self) -> bool { 16 | self.is_pending 17 | } 18 | 19 | /// Marks the updates in the given closure as transitions. 20 | pub fn start(&mut self, f: impl FnOnce() + 'static) { 21 | self 22 | .start_transition 23 | .call1(&JsValue::NULL, &Closure::once_into_js(f)) 24 | .expect_throw("unable to call start function"); 25 | } 26 | } 27 | 28 | /// Returns a stateful value for the pending state of the transition, and a 29 | /// function to start it. 30 | /// 31 | /// # Example 32 | /// 33 | /// ``` 34 | /// # use wasm_react::{*, hooks::*}; 35 | /// # 36 | /// # fn render() -> VNode { 37 | /// let count = use_state(|| 0); 38 | /// let transition = use_transition(); 39 | /// 40 | /// h!(div).build(( 41 | /// transition.is_pending().then(|| 42 | /// h!(div).build("Loading…") 43 | /// ), 44 | /// h!(button).on_click(&Callback::new({ 45 | /// clones!(count, mut transition); 46 | /// 47 | /// move |_| { 48 | /// transition.start({ 49 | /// clones!(mut count); 50 | /// 51 | /// move || { 52 | /// count.set(|c| c + 1); 53 | /// } 54 | /// }); 55 | /// } 56 | /// })).build("Increment"), 57 | /// )) 58 | /// # } 59 | /// ``` 60 | pub fn use_transition() -> Transition { 61 | let result = react_bindings::use_transition(); 62 | 63 | let is_pending = result 64 | .get(0) 65 | .as_bool() 66 | .expect_throw("unable to read pending state from transition"); 67 | let start_transition = result 68 | .get(1) 69 | .dyn_into::() 70 | .expect_throw("unable to read start function from transition"); 71 | 72 | Transition { 73 | is_pending, 74 | start_transition, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/04-context/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod button; 2 | mod card; 3 | 4 | use button::Button; 5 | use card::Card; 6 | use std::rc::Rc; 7 | use wasm_bindgen::JsValue; 8 | use wasm_react::{ 9 | clones, create_context, export_components, h, hooks::use_state, Callback, 10 | Component, Context, ContextProvider, VNode, 11 | }; 12 | 13 | pub enum Theme { 14 | LightMode, 15 | DarkMode, 16 | } 17 | 18 | thread_local! { 19 | pub static THEME_CONTEXT: Context 20 | = create_context(Theme::LightMode.into()); 21 | } 22 | 23 | pub struct App; 24 | 25 | impl Component for App { 26 | fn render(&self) -> VNode { 27 | let theme = use_state(|| Rc::new(Theme::LightMode)); 28 | let theme_class = match **theme.value() { 29 | Theme::LightMode => "light", 30 | Theme::DarkMode => "dark", 31 | }; 32 | 33 | let result = h!(div[.{theme_class}]).build( 34 | // 35 | ContextProvider::from(&THEME_CONTEXT) 36 | .value(Some({ 37 | let value = theme.value(); 38 | value.clone() 39 | })) 40 | .build(( 41 | h!(p).build(( 42 | // 43 | h!(label).build(( 44 | h!(input) 45 | .html_type("checkbox") 46 | .checked(match **theme.value() { 47 | Theme::LightMode => false, 48 | Theme::DarkMode => true, 49 | }) 50 | .on_change(&Callback::new({ 51 | clones!(mut theme); 52 | 53 | move |_| { 54 | theme.set(|theme| { 55 | match *theme { 56 | Theme::LightMode => Theme::DarkMode, 57 | Theme::DarkMode => Theme::LightMode, 58 | } 59 | .into() 60 | }) 61 | } 62 | })) 63 | .build(()), 64 | "Dark Mode", 65 | )), 66 | )), 67 | // 68 | Card::new().build(( 69 | h!(p).build("Hello World!"), 70 | h!(p).build(( 71 | Button::new().build("OK"), 72 | " ", 73 | Button::new().build("Cancel"), 74 | )), 75 | )), 76 | )), 77 | ); 78 | result 79 | } 80 | } 81 | 82 | impl TryFrom for App { 83 | type Error = JsValue; 84 | 85 | fn try_from(_: JsValue) -> Result { 86 | Ok(App) 87 | } 88 | } 89 | 90 | export_components! { App } 91 | -------------------------------------------------------------------------------- /src/react_bindings/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | hooks::{RefContainerValue, TmpRef}, 3 | ComponentWrapper, MemoComponentWrapper, 4 | }; 5 | use js_sys::{Array, Function}; 6 | use wasm_bindgen::prelude::*; 7 | 8 | #[wasm_bindgen(module = "/src/react_bindings/react-bindings.js")] 9 | extern "C" { 10 | #[wasm_bindgen(js_name = useReact)] 11 | pub fn use_react(value: &JsValue); 12 | 13 | #[wasm_bindgen(js_name = createElement)] 14 | pub fn create_element( 15 | typ: &JsValue, 16 | props: &JsValue, 17 | children: &JsValue, 18 | ) -> JsValue; 19 | 20 | #[wasm_bindgen(js_name = createRustComponent)] 21 | pub fn create_rust_component( 22 | name: &str, 23 | key: &JsValue, 24 | component: ComponentWrapper, 25 | ) -> JsValue; 26 | 27 | #[wasm_bindgen(js_name = createRustMemoComponent)] 28 | pub fn create_rust_memo_component( 29 | name: &str, 30 | key: &JsValue, 31 | component: MemoComponentWrapper, 32 | ) -> JsValue; 33 | 34 | #[wasm_bindgen(js_name = useRustRef)] 35 | pub fn use_rust_ref( 36 | create: &JsValue, 37 | callback: &mut dyn FnMut(&RefContainerValue), 38 | ); 39 | 40 | #[wasm_bindgen(js_name = useRustTmpRefs)] 41 | pub fn use_rust_tmp_refs(); 42 | 43 | #[wasm_bindgen(js_name = useRustTmpRef)] 44 | pub fn use_rust_tmp_ref(value: TmpRef, callback: &mut dyn FnMut(&TmpRef)); 45 | 46 | #[wasm_bindgen(js_name = useRustState)] 47 | pub fn use_rust_state() -> Function; 48 | 49 | #[wasm_bindgen(js_name = useRustEffect)] 50 | pub fn use_rust_effect(effect: &JsValue, dep: u8); 51 | 52 | #[wasm_bindgen(js_name = useRustLayoutEffect)] 53 | pub fn use_rust_layout_effect(effect: &JsValue, dep: u8); 54 | 55 | #[wasm_bindgen(js_name = useRustContext)] 56 | pub fn use_rust_context( 57 | context: &JsValue, 58 | callback: &mut dyn FnMut(&RefContainerValue), 59 | ); 60 | 61 | // From the React namespace: 62 | 63 | #[wasm_bindgen(js_namespace = React, js_name = Fragment)] 64 | pub static FRAGMENT: JsValue; 65 | 66 | #[wasm_bindgen(js_namespace = React, js_name = Suspense)] 67 | pub static SUSPENSE: JsValue; 68 | 69 | #[wasm_bindgen(js_namespace = React, js_name = useRef)] 70 | pub fn use_ref(init: &JsValue) -> JsValue; 71 | 72 | #[wasm_bindgen(js_namespace = React, js_name = useId)] 73 | pub fn use_id() -> String; 74 | 75 | #[wasm_bindgen(js_namespace = React, js_name = useDeferredValue)] 76 | pub fn use_deferred_value(value: u8) -> u8; 77 | 78 | #[wasm_bindgen(js_namespace = React, js_name = useTransition)] 79 | pub fn use_transition() -> Array; 80 | 81 | #[wasm_bindgen(js_namespace = React, js_name = createContext)] 82 | pub fn create_context(value: RefContainerValue) -> JsValue; 83 | } 84 | -------------------------------------------------------------------------------- /src/hooks/use_state.rs: -------------------------------------------------------------------------------- 1 | use super::{use_ref, RefContainer}; 2 | use crate::react_bindings; 3 | use js_sys::Function; 4 | use std::cell::Ref; 5 | use wasm_bindgen::{JsValue, UnwrapThrowExt}; 6 | 7 | /// Allows access to the underlying state data persisted with [`use_state()`]. 8 | #[derive(Debug)] 9 | pub struct State { 10 | ref_container: RefContainer>, 11 | update: Function, 12 | } 13 | 14 | impl State { 15 | /// Returns a reference to the value of the state. 16 | pub fn value(&self) -> Ref<'_, T> { 17 | Ref::map(self.ref_container.current(), |x| { 18 | x.as_ref().expect_throw("no state value available") 19 | }) 20 | } 21 | 22 | /// Sets the state to the return value of the given mutator closure and 23 | /// rerenders the component. 24 | /// 25 | /// # Panics 26 | /// 27 | /// Panics if the value is currently borrowed. 28 | pub fn set(&mut self, mutator: impl FnOnce(T) -> T) { 29 | let value = self.ref_container.current_mut().take(); 30 | let new_value = value.map(|value| mutator(value)); 31 | 32 | self.ref_container.set_current(new_value); 33 | self 34 | .update 35 | .call0(&JsValue::NULL) 36 | .expect_throw("unable to call state update"); 37 | } 38 | } 39 | 40 | impl Clone for State { 41 | fn clone(&self) -> Self { 42 | Self { 43 | ref_container: self.ref_container.clone(), 44 | update: self.update.clone(), 45 | } 46 | } 47 | } 48 | 49 | /// Persist stateful data of the component. 50 | /// 51 | /// Unlike the [`use_ref()`] hook, updating the state will automatically trigger 52 | /// a rerender of the component. 53 | /// 54 | /// Unlike its React counterpart, setting the state will mutate the underlying 55 | /// data immediately. 56 | /// 57 | /// # Example 58 | /// 59 | /// ``` 60 | /// # use wasm_react::{*, hooks::*}; 61 | /// # 62 | /// # struct State { greet: &'static str } 63 | /// # struct C; 64 | /// # impl C { 65 | /// fn render(&self) -> VNode { 66 | /// let state = use_state(|| State { greet: "Hello!" }); 67 | /// 68 | /// use_effect({ 69 | /// clones!(mut state); 70 | /// 71 | /// move || { 72 | /// state.set(|mut state| { 73 | /// state.greet = "Welcome!"; 74 | /// state 75 | /// }); 76 | /// } 77 | /// }, Deps::some(( /* … */ ))); 78 | /// 79 | /// let vnode = h!(div).build(state.value().greet); 80 | /// vnode 81 | /// } 82 | /// # } 83 | /// ``` 84 | pub fn use_state(init: impl FnOnce() -> T) -> State { 85 | let mut ref_container = use_ref(None); 86 | 87 | if ref_container.current().is_none() { 88 | ref_container.set_current(Some(init())); 89 | } 90 | 91 | let update = react_bindings::use_rust_state(); 92 | 93 | State { 94 | ref_container, 95 | update, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/hooks/use_js_ref.rs: -------------------------------------------------------------------------------- 1 | use crate::react_bindings; 2 | use js_sys::Reflect; 3 | use std::{fmt::Debug, marker::PhantomData}; 4 | use wasm_bindgen::{intern, JsCast, JsValue, UnwrapThrowExt}; 5 | 6 | /// Allows access to the underlying JS data persisted with [`use_js_ref()`]. 7 | pub struct JsRefContainer(JsValue, PhantomData); 8 | 9 | impl JsRefContainer { 10 | /// Returns the underlying typed JS data. 11 | pub fn current(&self) -> Option { 12 | self.current_untyped().dyn_into::().ok() 13 | } 14 | 15 | /// Returns the underlying JS data as [`JsValue`]. 16 | pub fn current_untyped(&self) -> JsValue { 17 | Reflect::get(&self.0, &intern("current").into()) 18 | .expect_throw("cannot read from ref container") 19 | } 20 | 21 | /// Sets the underlying JS data. 22 | pub fn set_current(&self, value: Option<&T>) { 23 | Reflect::set( 24 | &self.0, 25 | &intern("current").into(), 26 | value.map(|t| t.as_ref()).unwrap_or(&JsValue::null()), 27 | ) 28 | .expect_throw("cannot write into ref container"); 29 | } 30 | } 31 | 32 | impl Debug for JsRefContainer { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | f.debug_tuple("JsRefContainer").field(&self.0).finish() 35 | } 36 | } 37 | 38 | impl Clone for JsRefContainer { 39 | fn clone(&self) -> Self { 40 | Self(self.0.clone(), PhantomData) 41 | } 42 | } 43 | 44 | impl AsRef for JsRefContainer { 45 | fn as_ref(&self) -> &JsValue { 46 | &self.0 47 | } 48 | } 49 | 50 | impl From> for JsValue { 51 | fn from(value: JsRefContainer) -> Self { 52 | value.0 53 | } 54 | } 55 | 56 | impl From for JsRefContainer { 57 | fn from(value: JsValue) -> Self { 58 | Self(value, PhantomData) 59 | } 60 | } 61 | 62 | /// This hook can persist JS data through the entire lifetime of the component. 63 | /// 64 | /// Use this if you need JS to set the ref value. If you only need to mutate the 65 | /// data from Rust, use [`use_ref()`](crate::hooks::use_ref()) instead. 66 | /// 67 | /// # Example 68 | /// 69 | /// ``` 70 | /// # use wasm_react::{*, hooks::*}; 71 | /// # struct MyComponent; 72 | /// impl Component for MyComponent { 73 | /// fn render(&self) -> VNode { 74 | /// let input_element = use_js_ref(None); 75 | /// 76 | /// h!(div) 77 | /// .build( 78 | /// h!(input) 79 | /// .ref_container(&input_element) 80 | /// .html_type("text") 81 | /// .build(()) 82 | /// ) 83 | /// } 84 | /// } 85 | /// ``` 86 | pub fn use_js_ref(init: Option) -> JsRefContainer { 87 | let ref_container = react_bindings::use_ref( 88 | &init.map(|init| init.into()).unwrap_or(JsValue::null()), 89 | ); 90 | 91 | JsRefContainer(ref_container, PhantomData) 92 | } 93 | -------------------------------------------------------------------------------- /src/prop_container.rs: -------------------------------------------------------------------------------- 1 | use crate::hooks::{DeferredValue, Memo, RefContainer, State}; 2 | use std::{ 3 | cell::{Ref, RefCell}, 4 | ops::Deref, 5 | rc::Rc, 6 | }; 7 | 8 | /// Allows read-only access to the underlying value of [`PropContainer`]. 9 | #[non_exhaustive] 10 | #[derive(Debug)] 11 | pub enum PropContainerRef<'a, T> { 12 | #[allow(missing_docs)] 13 | Simple(&'a T), 14 | #[allow(missing_docs)] 15 | Ref(Ref<'a, T>), 16 | } 17 | 18 | impl PropContainerRef<'_, T> { 19 | /// Clones the reference. 20 | pub fn clone(orig: &Self) -> Self { 21 | match orig { 22 | PropContainerRef::Simple(x) => PropContainerRef::Simple(x), 23 | PropContainerRef::Ref(x) => PropContainerRef::Ref(Ref::clone(x)), 24 | } 25 | } 26 | } 27 | 28 | impl Deref for PropContainerRef<'_, T> { 29 | type Target = T; 30 | 31 | fn deref(&self) -> &Self::Target { 32 | match &self { 33 | PropContainerRef::Simple(x) => x, 34 | PropContainerRef::Ref(x) => x.deref(), 35 | } 36 | } 37 | } 38 | 39 | macro_rules! define_value_container { 40 | { 41 | $( 42 | $Variant:ident($id:ident: $Ty:ty) => $RefVariant:ident($expr:expr) $(,)? 43 | )* 44 | } => { 45 | /// A helpful abstraction over non-`Copy` types that can be used as a prop 46 | /// type for components. 47 | /// 48 | /// Can contain all hook containers, [`Rc`], and [`Rc>`]. 49 | #[non_exhaustive] 50 | #[derive(Debug)] 51 | pub enum PropContainer { 52 | $( 53 | #[allow(missing_docs)] 54 | $Variant($Ty), 55 | )* 56 | } 57 | 58 | impl PropContainer { 59 | /// Returns a read-only reference to the underlying value. 60 | pub fn value(&self) -> PropContainerRef<'_, T> { 61 | match self { 62 | $( Self::$Variant($id) => PropContainerRef::$RefVariant($expr), )* 63 | } 64 | } 65 | } 66 | 67 | impl Clone for PropContainer { 68 | fn clone(&self) -> Self { 69 | match self { 70 | $( Self::$Variant(x) => Self::$Variant(x.clone()), )* 71 | } 72 | } 73 | } 74 | 75 | $( 76 | impl From<$Ty> for PropContainer { 77 | fn from(value: $Ty) -> Self { 78 | Self::$Variant(value) 79 | } 80 | } 81 | )* 82 | }; 83 | } 84 | 85 | define_value_container! { 86 | Rc(x: Rc) => Simple(x.deref()), 87 | RcRefCell(x: Rc>) => Ref(x.borrow()), 88 | RefContainer(x: RefContainer) => Ref(x.current()), 89 | State(x: State) => Ref(x.value()), 90 | Memo(x: Memo) => Ref(x.value()), 91 | DeferredValue(x: DeferredValue) => Ref(x.value()), 92 | } 93 | 94 | impl PartialEq for PropContainer { 95 | fn eq(&self, other: &Self) -> bool { 96 | T::eq(&self.value(), &other.value()) 97 | } 98 | } 99 | 100 | impl PartialEq for PropContainer { 101 | fn eq(&self, other: &T) -> bool { 102 | T::eq(&self.value(), other) 103 | } 104 | } 105 | 106 | impl From for PropContainer { 107 | fn from(value: T) -> Self { 108 | Self::from(Rc::new(value)) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/props/h.rs: -------------------------------------------------------------------------------- 1 | use super::Props; 2 | use crate::{ 3 | Callback, create_element, hooks::JsRefContainer, KeyType, VNode, 4 | }; 5 | use std::borrow::Cow; 6 | use wasm_bindgen::{ 7 | convert::{FromWasmAbi, IntoWasmAbi}, 8 | intern, JsValue, 9 | }; 10 | use web_sys::Element; 11 | 12 | #[doc(hidden)] 13 | #[derive(Debug, Clone, Copy)] 14 | pub struct HtmlTag<'a>(pub &'a str); 15 | 16 | impl AsRef for HtmlTag<'_> { 17 | fn as_ref(&self) -> &str { 18 | &self.0 19 | } 20 | } 21 | 22 | /// A marker trait for the component type that [`H`] is supposed to build. 23 | /// 24 | /// Can either be `HtmlTag` or any imported component. 25 | pub trait HType { 26 | /// Returns a reference to the [`JsValue`] of this component type. 27 | fn as_js(&self) -> Cow<'_, JsValue>; 28 | } 29 | 30 | impl HType for HtmlTag<'_> { 31 | fn as_js(&self) -> Cow<'_, JsValue> { 32 | Cow::Owned(intern(self.0).into()) 33 | } 34 | } 35 | 36 | /// The component builder that powers [`h!`](crate::h!), which provides 37 | /// convenience methods for adding props. 38 | /// 39 | /// In case `T` is `HtmlTag`, [`H`] also provides auto-completion for HTML 40 | /// attributes and events. 41 | #[derive(Debug, Clone)] 42 | pub struct H { 43 | pub(crate) typ: T, 44 | pub(crate) props: Props, 45 | } 46 | 47 | impl H { 48 | /// Creates a new instance of [`H`]. It is recommended to use the 49 | /// [`h!`](crate::h!) macro instead. 50 | pub fn new(typ: T) -> Self { 51 | Self { 52 | typ, 53 | props: Props::new(), 54 | } 55 | } 56 | 57 | /// Sets the [React key][key]. 58 | /// 59 | /// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key 60 | pub fn key(mut self, value: Option) -> Self { 61 | self.props = self.props.key(value); 62 | self 63 | } 64 | 65 | /// Sets the [React ref][ref] to the given ref container created with the 66 | /// [`use_js_ref()`](crate::hooks::use_js_ref()) hook. 67 | /// 68 | /// [ref]: https://react.dev/learn/manipulating-the-dom-with-refs 69 | pub fn ref_container( 70 | mut self, 71 | ref_container: &JsRefContainer, 72 | ) -> Self { 73 | self.props = self.props.ref_container(ref_container); 74 | self 75 | } 76 | 77 | /// Sets the [React ref][ref] to the given ref callback. 78 | /// 79 | /// [ref]: https://react.dev/learn/manipulating-the-dom-with-refs 80 | pub fn ref_callback( 81 | mut self, 82 | ref_callback: &Callback>, 83 | ) -> Self { 84 | self.props = self.props.ref_callback(ref_callback); 85 | self 86 | } 87 | 88 | /// Sets an attribute on the [`VNode`]. 89 | pub fn attr(mut self, key: &str, value: &JsValue) -> Self { 90 | self.props = self.props.insert(key, value); 91 | self 92 | } 93 | 94 | /// Sets a callback value to an attribute on the [`VNode`]. 95 | pub fn attr_callback(mut self, key: &str, f: &Callback) -> Self 96 | where 97 | U: FromWasmAbi + 'static, 98 | V: IntoWasmAbi + 'static, 99 | { 100 | self.props = self.props.insert_callback(key, f); 101 | self 102 | } 103 | 104 | /// Builds the [`VNode`] and returns it with the given children. 105 | pub fn build(self, children: impl Into) -> VNode { 106 | create_element(&self.typ.as_js(), &self.props, children.into()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/props/props.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | hooks::{use_tmp_ref, JsRefContainer}, 3 | Callback, KeyType, 4 | }; 5 | use js_sys::{Object, Reflect}; 6 | use wasm_bindgen::{ 7 | convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi}, 8 | intern, JsCast, JsValue, UnwrapThrowExt, 9 | }; 10 | 11 | /// A convenience builder for JS objects. Mainly used for constructing props 12 | /// that are not controlled by Rust. 13 | /// 14 | /// Use [`Style`](super::Style) to create style objects which also provides 15 | /// auto-completion. 16 | /// 17 | /// # Example 18 | /// 19 | /// ``` 20 | /// # use wasm_react::{*, props::*}; 21 | /// # use wasm_bindgen::prelude::*; 22 | /// # 23 | /// # fn f(handle_click: &Callback) -> Props { 24 | /// Props::new() 25 | /// .insert("id", &"app".into()) 26 | /// .insert_callback("onClick", handle_click) 27 | /// # } 28 | /// ``` 29 | #[derive(Debug, Default, Clone)] 30 | pub struct Props(Object); 31 | 32 | impl Props { 33 | /// Creates a new, empty object. 34 | pub fn new() -> Self { 35 | Self::default() 36 | } 37 | 38 | /// Sets the [React key][key]. 39 | /// 40 | /// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key 41 | pub fn key(self, value: Option) -> Self { 42 | self.insert( 43 | "key", 44 | &value.map(|x| x.into()).unwrap_or(JsValue::UNDEFINED), 45 | ) 46 | } 47 | 48 | /// Sets the [React ref][ref] to the given ref container created with the 49 | /// [`use_js_ref()`](crate::hooks::use_js_ref()) hook. 50 | /// 51 | /// [ref]: https://react.dev/learn/manipulating-the-dom-with-refs 52 | pub fn ref_container(self, ref_container: &JsRefContainer) -> Self { 53 | self.insert("ref", ref_container.as_ref()) 54 | } 55 | 56 | /// Sets the [React ref][ref] to the given ref callback. 57 | /// 58 | /// [ref]: https://react.dev/learn/manipulating-the-dom-with-refs 59 | pub fn ref_callback(self, ref_callback: &Callback>) -> Self 60 | where 61 | T: OptionFromWasmAbi + 'static, 62 | { 63 | self.insert_callback("ref", ref_callback) 64 | } 65 | 66 | /// Equivalent to `props[key] = value;`. 67 | pub fn insert(self, key: &str, value: &JsValue) -> Self { 68 | self.ref_insert(key, value); 69 | self 70 | } 71 | 72 | fn ref_insert(&self, key: &str, value: &JsValue) { 73 | Reflect::set(&self.0, &intern(key).into(), value) 74 | .expect_throw("cannot write into props object"); 75 | } 76 | 77 | /// Equivalent to `props[key] = f;`. 78 | pub fn insert_callback(self, key: &str, f: &Callback) -> Self 79 | where 80 | T: FromWasmAbi + 'static, 81 | U: IntoWasmAbi + 'static, 82 | { 83 | use_tmp_ref(f.clone(), |f| { 84 | self.ref_insert(key, &f.as_js()); 85 | }); 86 | 87 | self 88 | } 89 | } 90 | 91 | impl AsRef for Props { 92 | fn as_ref(&self) -> &JsValue { 93 | &self.0 94 | } 95 | } 96 | 97 | impl From for JsValue { 98 | fn from(style: Props) -> Self { 99 | style.0.into() 100 | } 101 | } 102 | 103 | impl From for Props { 104 | fn from(value: Object) -> Self { 105 | Props(value) 106 | } 107 | } 108 | 109 | impl TryFrom for Props { 110 | type Error = JsValue; 111 | 112 | fn try_from(value: JsValue) -> Result { 113 | Ok(Props(value.dyn_into::()?)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/03-material-ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use wasm_react::{ 3 | export_components, import_components, 4 | props::{Style, H}, 5 | Component, 6 | }; 7 | 8 | import_components! { 9 | #[wasm_bindgen(module = "/js/mui-components.js")] 10 | AppBar, Toolbar, Typography, IconButton, Button, Box as BoxComponent, 11 | Container, Card, CardContent, CardActions, MenuIcon 12 | } 13 | 14 | pub trait HMuiComponentExt { 15 | fn sx(self, style: &Style) -> Self; 16 | } 17 | 18 | macro_rules! impl_mui_component { 19 | { $( $Component:ty ),* } => { 20 | $( 21 | impl HMuiComponentExt for H<$Component> { 22 | fn sx(self, style: &Style) -> Self { 23 | self.attr("sx", style.as_ref()) 24 | } 25 | } 26 | )* 27 | }; 28 | } 29 | 30 | impl_mui_component! { 31 | AppBar, Toolbar, Typography, IconButton, Button, BoxComponent, 32 | Container, Card, CardContent, CardActions, MenuIcon 33 | } 34 | 35 | pub struct App; 36 | 37 | impl Component for App { 38 | fn render(&self) -> wasm_react::VNode { 39 | BoxComponent::new().build(( 40 | AppBar::new().build( 41 | // 42 | Toolbar::new().build(( 43 | IconButton::new() 44 | .attr("color", &"inherit".into()) 45 | .attr("edge", &"start".into()) 46 | .sx(&Style::new().margin_right(2)) 47 | .build(MenuIcon::new().build(())), 48 | Typography::new() 49 | .attr("variant", &"h6".into()) 50 | .attr("color", &"inherit".into()) 51 | .attr("component", &"h1".into()) 52 | .sx(&Style::new().flex_grow(1)) 53 | .build("MUI Example Application"), 54 | )), 55 | ), 56 | // 57 | Container::new() 58 | .sx(&Style::new().margin_top(8).padding_top(2).padding_bottom(2)) 59 | .build( 60 | // 61 | Card::new() 62 | .attr("variant", &"outlined".into()) 63 | .sx(&Style::new().max_width(345)) 64 | .build(( 65 | CardContent::new().build(( 66 | Typography::new() 67 | .attr("variant", &"h5".into()) 68 | .attr("component", &"h2".into()) 69 | .sx(&Style::new().margin_bottom(1.5)) 70 | .build("Hello World!"), 71 | Typography::new().attr("variant", &"body2".into()).build( 72 | r"Lorem ipsum dolor sit amet, consectetur adipiscing elit, 73 | sed do eiusmod tempor incididunt ut labore et dolore magna 74 | aliqua. Ut enim ad minim veniam, quis nostrud exercitation 75 | ullamco laboris nisi ut aliquip ex ea commodo consequat. 76 | Duis aute irure dolor in reprehenderit in voluptate velit 77 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 78 | occaecat cupidatat non proident, sunt in culpa qui officia 79 | deserunt mollit anim id est laborum.", 80 | ), 81 | )), 82 | CardActions::new().build( 83 | // 84 | Button::new() 85 | .attr("size", &"small".into()) 86 | .build("Learn More"), 87 | ), 88 | )), 89 | ), 90 | )) 91 | } 92 | } 93 | 94 | impl TryFrom for App { 95 | type Error = JsValue; 96 | 97 | fn try_from(_: JsValue) -> Result { 98 | Ok(App) 99 | } 100 | } 101 | 102 | export_components! { App } 103 | -------------------------------------------------------------------------------- /src/hooks/use_effect.rs: -------------------------------------------------------------------------------- 1 | use super::{use_ref, Deps}; 2 | use crate::react_bindings; 3 | use wasm_bindgen::{prelude::Closure, JsValue, UnwrapThrowExt}; 4 | 5 | /// Denotes types that can be used as destructors for effects. 6 | pub trait IntoDestructor { 7 | #[doc(hidden)] 8 | type Destructor: FnOnce() + 'static; 9 | 10 | #[doc(hidden)] 11 | fn into_destructor(self) -> Self::Destructor; 12 | } 13 | 14 | impl IntoDestructor for () { 15 | type Destructor = fn(); 16 | 17 | fn into_destructor(self) -> Self::Destructor { 18 | || () 19 | } 20 | } 21 | 22 | impl IntoDestructor for F 23 | where 24 | F: FnOnce() + 'static, 25 | { 26 | type Destructor = F; 27 | 28 | fn into_destructor(self) -> Self::Destructor { 29 | self 30 | } 31 | } 32 | 33 | fn use_effect_inner( 34 | effect: impl FnOnce() -> G + 'static, 35 | deps: Deps, 36 | f: impl FnOnce(&JsValue, u8), 37 | ) where 38 | G: IntoDestructor, 39 | D: PartialEq + 'static, 40 | { 41 | let create_effect_closure = move || { 42 | Closure::once(move || { 43 | let destructor = effect(); 44 | 45 | // The effect destructor will definitely be called exactly once by React 46 | Closure::once_into_js(destructor.into_destructor()) 47 | }) 48 | }; 49 | 50 | let mut ref_container = 51 | use_ref(None::<(Closure JsValue>, Deps, u8)>); 52 | 53 | let new_value = match ref_container.current_mut().take() { 54 | Some((old_effect, old_deps, counter)) => { 55 | if deps.is_all() || old_deps != deps { 56 | Some((create_effect_closure(), deps, counter.wrapping_add(1))) 57 | } else { 58 | // Dependencies didn't change 59 | Some((old_effect, old_deps, counter)) 60 | } 61 | } 62 | None => Some((create_effect_closure(), deps, 0)), 63 | }; 64 | 65 | ref_container.set_current(new_value); 66 | 67 | let value = ref_container.current(); 68 | let (effect, _, counter) = 69 | value.as_ref().expect_throw("no effect data available"); 70 | 71 | f(effect.as_ref(), *counter); 72 | } 73 | 74 | /// Runs a function which contains imperative code that may cause side-effects. 75 | /// 76 | /// The given function will run after render is committed to the screen when 77 | /// the given dependencies have changed from last render. The function can 78 | /// return a clean-up function. 79 | /// 80 | /// # Example 81 | /// 82 | /// ``` 83 | /// # use wasm_react::{*, hooks::*}; 84 | /// # 85 | /// # fn fetch(url: &str) -> String { String::new() } 86 | /// # struct C { url: &'static str } 87 | /// # impl C { 88 | /// # fn f(&self) { 89 | /// let state = use_state(|| None); 90 | /// 91 | /// use_effect({ 92 | /// clones!(self.url, mut state); 93 | /// 94 | /// move || { 95 | /// state.set(|_| Some(fetch(url))); 96 | /// } 97 | /// }, Deps::some(self.url)); 98 | /// # 99 | /// # } 100 | /// # } 101 | /// ``` 102 | pub fn use_effect(effect: impl FnOnce() -> G + 'static, deps: Deps) 103 | where 104 | G: IntoDestructor, 105 | D: PartialEq + 'static, 106 | { 107 | use_effect_inner(effect, deps, react_bindings::use_rust_effect); 108 | } 109 | 110 | /// Same as [`use_effect()`], but it fires synchronously after all DOM mutations. 111 | /// 112 | /// See [React documentation](https://react.dev/reference/react/useLayoutEffect). 113 | pub fn use_layout_effect( 114 | effect: impl FnOnce() -> G + 'static, 115 | deps: Deps, 116 | ) where 117 | G: IntoDestructor, 118 | D: PartialEq + 'static, 119 | { 120 | use_effect_inner(effect, deps, react_bindings::use_rust_layout_effect); 121 | } 122 | -------------------------------------------------------------------------------- /src/vnode.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use js_sys::{Array, JsString}; 4 | use wasm_bindgen::JsValue; 5 | 6 | /// Represents a node in the virtual DOM of React. 7 | #[non_exhaustive] 8 | #[derive(Debug, Clone)] 9 | pub enum VNode { 10 | /// Represents a single virtual node. 11 | Single(JsValue), 12 | /// Represents an array of virtual nodes. 13 | List(Array), 14 | } 15 | 16 | impl VNode { 17 | /// Creates an empty node that doesn't render anything. 18 | pub fn new() -> VNode { 19 | VNode::Single(JsValue::null()) 20 | } 21 | 22 | /// Adds the given node to the list. 23 | pub fn push(&mut self, node: &VNode) { 24 | match self { 25 | VNode::Single(x) => { 26 | *self = VNode::List({ 27 | let arr = Array::new(); 28 | 29 | if !x.is_null() { 30 | arr.push(x); 31 | } 32 | 33 | arr.push(node.as_ref()); 34 | arr 35 | }); 36 | } 37 | VNode::List(arr) => { 38 | arr.push(node.as_ref()); 39 | } 40 | } 41 | } 42 | } 43 | 44 | impl Default for VNode { 45 | fn default() -> Self { 46 | Self::new() 47 | } 48 | } 49 | 50 | impl AsRef for VNode { 51 | fn as_ref(&self) -> &JsValue { 52 | match self { 53 | VNode::Single(x) => x, 54 | VNode::List(x) => x, 55 | } 56 | } 57 | } 58 | 59 | impl From for JsValue { 60 | fn from(value: VNode) -> Self { 61 | match value { 62 | VNode::Single(x) => x, 63 | VNode::List(x) => x.into(), 64 | } 65 | } 66 | } 67 | 68 | impl> From> for VNode { 69 | fn from(value: Option) -> Self { 70 | value.map(|value| value.into()).unwrap_or_default() 71 | } 72 | } 73 | 74 | impl Extend for VNode { 75 | fn extend>(&mut self, iter: T) { 76 | for node in iter.into_iter() { 77 | self.push(&node); 78 | } 79 | } 80 | } 81 | 82 | impl FromIterator for VNode { 83 | fn from_iter>(iter: T) -> Self { 84 | let mut result = Self::new(); 85 | 86 | for node in iter.into_iter() { 87 | result.push(&node); 88 | } 89 | 90 | result 91 | } 92 | } 93 | 94 | macro_rules! impl_into_vnode { 95 | { $( $T:ty ),* $(,)? } => { 96 | $( 97 | impl From<$T> for VNode { 98 | fn from(value: $T) -> Self { 99 | VNode::Single(value.into()) 100 | } 101 | } 102 | )* 103 | }; 104 | } 105 | 106 | // Implement `Into` for as many `Display` types as possible 107 | impl_into_vnode! { 108 | &str, String, JsString, 109 | f32, f64, 110 | i8, i16, i32, i64, i128, isize, 111 | u8, u16, u32, u64, u128, usize, 112 | } 113 | 114 | impl From<()> for VNode { 115 | fn from(_: ()) -> Self { 116 | VNode::new() 117 | } 118 | } 119 | 120 | macro_rules! impl_into_vnode_for_tuples { 121 | (@impl) => {}; 122 | (@impl $( $x:ident ),+) => { 123 | impl<$( $x, )+> From<($( $x, )+)> for VNode 124 | where $( $x: Into, )+ 125 | { 126 | fn from(($( $x, )+): ($( $x, )+)) -> VNode { 127 | let mut result = VNode::new(); 128 | $( result.push(&$x.into()); )+ 129 | result 130 | } 131 | } 132 | 133 | impl_into_vnode_for_tuples!(@next $( $x ),+); 134 | }; 135 | (@next $first:ident) => {}; 136 | (@next $first:ident, $( $tt:tt )*) => { 137 | impl_into_vnode_for_tuples!(@impl $( $tt )*); 138 | }; 139 | ($( $x:ident ),*) => { 140 | impl_into_vnode_for_tuples!(@impl $( $x ),*); 141 | } 142 | } 143 | 144 | impl_into_vnode_for_tuples!( 145 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z 146 | ); 147 | -------------------------------------------------------------------------------- /src/hooks/use_ref.rs: -------------------------------------------------------------------------------- 1 | use crate::react_bindings; 2 | use std::{ 3 | any::Any, 4 | cell::{Ref, RefCell, RefMut}, 5 | fmt::Debug, 6 | rc::Rc, 7 | }; 8 | use wasm_bindgen::prelude::*; 9 | 10 | #[doc(hidden)] 11 | #[wasm_bindgen(js_name = __WasmReact_RefContainerValue)] 12 | #[derive(Debug, Clone)] 13 | pub struct RefContainerValue(pub(crate) Rc); 14 | 15 | impl RefContainerValue { 16 | pub fn value(&self) -> Result, Rc> { 17 | Rc::downcast::(self.0.clone()) 18 | } 19 | } 20 | 21 | /// Allows access to the underlying data persisted with [`use_ref()`]. 22 | /// 23 | /// # Panics 24 | /// 25 | /// The rules of borrowing will be enforced at runtime through a [`RefCell`], 26 | /// therefore the methods [`RefContainer::current()`], 27 | /// [`RefContainer::current_mut()`], and [`RefContainer::set_current()`] may 28 | /// panic accordingly. 29 | #[derive(Debug)] 30 | pub struct RefContainer(Rc>); 31 | 32 | impl RefContainer { 33 | /// Returns a reference to the underlying data. 34 | /// 35 | /// # Panics 36 | /// 37 | /// Panics if the underlying data is currently mutably borrowed. 38 | pub fn current(&self) -> Ref<'_, T> { 39 | self.0.borrow() 40 | } 41 | 42 | /// Returns a mutable reference to the underlying data. 43 | /// 44 | /// # Panics 45 | /// 46 | /// Panics if the underlying data is currently borrowed. 47 | pub fn current_mut(&mut self) -> RefMut<'_, T> { 48 | self.0.borrow_mut() 49 | } 50 | 51 | /// Sets the underlying data to the given value. 52 | /// 53 | /// # Panics 54 | /// 55 | /// Panics if the underlying data is currently borrowed. 56 | pub fn set_current(&mut self, value: T) { 57 | *self.current_mut() = value; 58 | } 59 | } 60 | 61 | impl Clone for RefContainer { 62 | fn clone(&self) -> Self { 63 | Self(self.0.clone()) 64 | } 65 | } 66 | 67 | /// This is the main hook for persisting Rust data through the entire lifetime 68 | /// of the component. 69 | /// 70 | /// Whenever the component is unmounted by React, the data will also be dropped. 71 | /// Keep in mind that the inner value of [`use_ref()`] can only be accessed in 72 | /// Rust. If you need a ref to hold a DOM element (or a JS value in general), 73 | /// use [`use_js_ref()`](crate::hooks::use_js_ref()) instead. 74 | /// 75 | /// The component will not rerender when you mutate the underlying data. If you 76 | /// want that, use [`use_state()`](crate::hooks::use_state()) instead. 77 | /// 78 | /// # Example 79 | /// 80 | /// ``` 81 | /// # use wasm_react::{*, hooks::*}; 82 | /// # struct MyData { value: &'static str }; 83 | /// # struct MyComponent { value: &'static str }; 84 | /// # 85 | /// impl Component for MyComponent { 86 | /// fn render(&self) -> VNode { 87 | /// let ref_container = use_ref(MyData { 88 | /// value: "Hello World!" 89 | /// }); 90 | /// 91 | /// use_effect({ 92 | /// clones!(self.value, mut ref_container); 93 | /// 94 | /// move || { 95 | /// ref_container.current_mut().value = value; 96 | /// } 97 | /// }, Deps::some(self.value)); 98 | /// 99 | /// let vnode = h!(div).build( 100 | /// ref_container.current().value 101 | /// ); 102 | /// vnode 103 | /// } 104 | /// } 105 | /// ``` 106 | pub fn use_ref(init: T) -> RefContainer { 107 | let mut value = None; 108 | 109 | react_bindings::use_rust_ref( 110 | Closure::once(move || RefContainerValue(Rc::new(RefCell::new(init)))) 111 | .as_ref(), 112 | &mut |ref_container_value| { 113 | value = Some( 114 | ref_container_value 115 | .value::>() 116 | .expect_throw("mismatched ref container type"), 117 | ); 118 | }, 119 | ); 120 | 121 | RefContainer(value.expect_throw("callback was not called")) 122 | } 123 | -------------------------------------------------------------------------------- /src/react_bindings/react-bindings.js: -------------------------------------------------------------------------------- 1 | const components = {}; 2 | 3 | export let React = undefined; 4 | 5 | export function useReact(value) { 6 | if (React == null) { 7 | React = value; 8 | } 9 | } 10 | 11 | export function createElement(name, props, children) { 12 | if (!Array.isArray(children)) children = [children]; 13 | return React.createElement(name, props, ...children); 14 | } 15 | 16 | let currentTmpRefs = null; 17 | 18 | function renderRustComponent(props) { 19 | // `component` is a `ComponentWrapper` or `MemoComponentWrapper` 20 | let component = props.component; 21 | 22 | // We need to free up the memory on Rust side whenever the old props 23 | // are replaced with new ones. 24 | React.useEffect( 25 | function freeProps() { 26 | return () => component.free(); 27 | }, 28 | [component] 29 | ); 30 | 31 | useRustTmpRefs(); 32 | return component.render(); 33 | } 34 | 35 | function getRustComponent(name) { 36 | if (components[name] == null) { 37 | // All Rust components have the same implementation in JS, but we need to 38 | // define them separately, so that React can distinguish them as different 39 | // components, and also so the names show up correctly in the React 40 | // Developer Tools. 41 | Object.assign(components, { 42 | [name]: (props = {}) => renderRustComponent(props), 43 | }); 44 | 45 | components[name].displayName = name; 46 | } 47 | 48 | return components[name]; 49 | } 50 | 51 | export function createRustComponent(name, key, component) { 52 | return React.createElement(getRustComponent(name), { 53 | key, 54 | component, 55 | }); 56 | } 57 | 58 | function getRustMemoComponent(name) { 59 | const key = `wasm_react::Memoized<${name}>`; 60 | 61 | if (components[key] == null) { 62 | Object.assign(components, { 63 | [key]: React.memo(getRustComponent(name), (prevProps, nextProps) => { 64 | // `component` is a `MemoComponentWrapper` 65 | const equal = prevProps.component.eq(nextProps.component); 66 | 67 | if (equal) { 68 | // Since rerender is going to be prevented, we need to dispose of 69 | // `nextProps` manually. 70 | nextProps.component.free(); 71 | } 72 | 73 | return equal; 74 | }), 75 | }); 76 | 77 | components[key].displayName = key; 78 | } 79 | 80 | return components[key]; 81 | } 82 | 83 | export function createRustMemoComponent(name, key, component) { 84 | return React.createElement(getRustMemoComponent(name), { 85 | key, 86 | component, 87 | }); 88 | } 89 | 90 | export function useRustRef(create, callback) { 91 | let ref = React.useRef(null); 92 | 93 | if (ref.current == null) { 94 | // Create ref struct if called for the first time. 95 | 96 | ref.current = create(); 97 | } 98 | 99 | React.useEffect(function freeRef() { 100 | return () => ref.current.free(); 101 | }, []); 102 | 103 | callback(ref.current); 104 | } 105 | 106 | export function useRustTmpRefs() { 107 | // Create storage for temporary refs, refs that are only valid until 108 | // next render 109 | currentTmpRefs = React.useRef([]); 110 | let tmpRefs = currentTmpRefs; 111 | const tmpRefsToBeFreed = tmpRefs.current; 112 | tmpRefs.current = []; 113 | 114 | React.useEffect(function freeTmpRefs() { 115 | return () => { 116 | setTimeout(() => { 117 | for (const value of tmpRefsToBeFreed) { 118 | value.free(); 119 | } 120 | }); 121 | }; 122 | }); 123 | } 124 | 125 | export function useRustTmpRef(value, callback) { 126 | if (currentTmpRefs != null) { 127 | currentTmpRefs.current.push(value); 128 | callback(value); 129 | } else { 130 | value.free(); 131 | } 132 | } 133 | 134 | export function useRustState() { 135 | // This only returns a function that can trigger a component rerender 136 | let [, setState] = React.useState(() => []); 137 | 138 | return () => setState([]); 139 | } 140 | 141 | export function useRustEffect(effect, dep) { 142 | React.useEffect(effect, [dep]); 143 | } 144 | 145 | export function useRustLayoutEffect(effect, dep) { 146 | React.useLayoutEffect(effect, [dep]); 147 | } 148 | 149 | export function useRustContext(context, callback) { 150 | callback(React.useContext(context)); 151 | } 152 | -------------------------------------------------------------------------------- /examples/02-todo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; 3 | use wasm_react::{ 4 | clones, export_components, h, hooks::use_state, Callback, Component, 5 | PropContainer, VNode, 6 | }; 7 | use web_sys::{Event, HtmlInputElement}; 8 | 9 | pub struct App; 10 | 11 | impl TryFrom for App { 12 | type Error = JsValue; 13 | 14 | fn try_from(_: JsValue) -> Result { 15 | Ok(App) 16 | } 17 | } 18 | 19 | impl Component for App { 20 | fn render(&self) -> VNode { 21 | let tasks = use_state(|| vec![]); 22 | let text = use_state(|| Rc::::from("")); 23 | 24 | let result = h!(div[#"app"]).build(( 25 | h!(h1).build("Todo"), 26 | // 27 | TaskList { 28 | tasks: tasks.clone().into(), 29 | on_change: Some(Callback::new({ 30 | clones!(mut tasks); 31 | 32 | move |(id, done)| { 33 | tasks.set(|mut tasks| { 34 | tasks.get_mut(id).map(|task: &mut (bool, _)| task.0 = done); 35 | tasks 36 | }) 37 | } 38 | })), 39 | } 40 | .build(), 41 | // 42 | h!(form) 43 | .on_submit(&Callback::new({ 44 | clones!(mut tasks, mut text); 45 | 46 | move |evt: Event| { 47 | evt.prevent_default(); 48 | 49 | if !text.value().is_empty() { 50 | tasks.set(|mut tasks| { 51 | tasks.push((false, text.value().clone())); 52 | tasks 53 | }); 54 | text.set(|_| "".into()); 55 | } 56 | } 57 | })) 58 | .build(( 59 | h!(input) 60 | .placeholder("Add new item…") 61 | .value(&**text.value()) 62 | .on_change(&Callback::new({ 63 | clones!(mut text); 64 | 65 | move |evt: Event| { 66 | text.set(|_| { 67 | evt 68 | .current_target() 69 | .unwrap_throw() 70 | .dyn_into::() 71 | .unwrap_throw() 72 | .value() 73 | .into() 74 | }) 75 | } 76 | })) 77 | .build(()), 78 | " ", 79 | h!(button).html_type("submit").build("Add"), 80 | )), 81 | )); 82 | 83 | result 84 | } 85 | } 86 | 87 | export_components! { 88 | /// This is the entry component for our Todo application 89 | App 90 | } 91 | 92 | struct TaskList { 93 | tasks: PropContainer)>>, 94 | on_change: Option>, 95 | } 96 | 97 | impl Component for TaskList { 98 | fn render(&self) -> VNode { 99 | h!(div[."task-list"]).build( 100 | // 101 | h!(ul).build( 102 | self 103 | .tasks 104 | .value() 105 | .iter() 106 | .enumerate() 107 | .map(|(i, (done, description))| { 108 | TaskItem { 109 | id: i, 110 | description: description.clone(), 111 | done: *done, 112 | on_change: self.on_change.clone(), 113 | } 114 | .memoized() 115 | .key(Some(i)) 116 | .build() 117 | }) 118 | .collect::(), 119 | ), 120 | ) 121 | } 122 | } 123 | 124 | #[derive(Debug, PartialEq)] 125 | struct TaskItem { 126 | id: usize, 127 | description: Rc, 128 | done: bool, 129 | on_change: Option>, 130 | } 131 | 132 | impl Component for TaskItem { 133 | fn render(&self) -> VNode { 134 | h!(li[."task-item"]).build( 135 | // 136 | h!(label).build(( 137 | h!(input) 138 | .html_type("checkbox") 139 | .checked(self.done) 140 | .on_change(&{ 141 | let id = self.id; 142 | 143 | self.on_change.clone().unwrap_or_default().premap( 144 | move |evt: Event| { 145 | ( 146 | id, 147 | evt 148 | .current_target() 149 | .unwrap_throw() 150 | .dyn_into::() 151 | .unwrap_throw() 152 | .checked(), 153 | ) 154 | }, 155 | ) 156 | }) 157 | .build(()), 158 | " ", 159 | if self.done { 160 | h!(del).build(&*self.description) 161 | } else { 162 | (*self.description).into() 163 | }, 164 | )), 165 | ) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | create_element, hooks::RefContainerValue, props::Props, react_bindings, 3 | Component, VNode, 4 | }; 5 | use js_sys::Reflect; 6 | use std::{marker::PhantomData, rc::Rc, thread::LocalKey}; 7 | use wasm_bindgen::{intern, JsValue, UnwrapThrowExt}; 8 | 9 | /// Represents a [React context][context] that can hold a global state. 10 | /// 11 | /// See [`create_context()`] for usage. 12 | /// 13 | /// [context]: https://react.dev/learn/passing-data-deeply-with-context 14 | #[derive(Debug)] 15 | pub struct Context { 16 | js_context: JsValue, 17 | phantom: PhantomData, 18 | } 19 | 20 | impl AsRef for Context { 21 | fn as_ref(&self) -> &JsValue { 22 | &self.js_context 23 | } 24 | } 25 | 26 | impl From> for JsValue { 27 | fn from(value: Context) -> Self { 28 | value.js_context 29 | } 30 | } 31 | 32 | /// Creates a new [React context][context] that can hold a global state. 33 | /// 34 | /// Use [`ContextProvider`] to make the context available for its subtrees and 35 | /// [`use_context()`](crate::hooks::use_context()) to get access to the context 36 | /// value. 37 | /// 38 | /// [context]: https://react.dev/learn/passing-data-deeply-with-context 39 | /// 40 | /// # Example 41 | /// 42 | /// ``` 43 | /// # use wasm_react::{*, hooks::*, props::*}; 44 | /// # pub enum Theme { DarkMode, LightMode } 45 | /// # 46 | /// thread_local! { 47 | /// // Pass in a default value for the context. 48 | /// static THEME_CONTEXT: Context = create_context(Theme::LightMode.into()); 49 | /// } 50 | /// 51 | /// struct App; 52 | /// 53 | /// impl Component for App { 54 | /// fn render(&self) -> VNode { 55 | /// // Use a `ContextProvider` to pass the context value to the trees below. 56 | /// // In this example, we are passing down `Theme::DarkMode`. 57 | /// 58 | /// ContextProvider::from(&THEME_CONTEXT) 59 | /// .value(Some(Theme::DarkMode.into())) 60 | /// .build( 61 | /// Toolbar.build(), 62 | /// ) 63 | /// } 64 | /// } 65 | /// 66 | /// struct Toolbar; 67 | /// 68 | /// impl Component for Toolbar { 69 | /// fn render(&self) -> VNode { 70 | /// // Theme context does not have to be passed down explicitly. 71 | /// h!(div).build(Button.build()) 72 | /// } 73 | /// } 74 | /// 75 | /// struct Button; 76 | /// 77 | /// impl Component for Button { 78 | /// fn render(&self) -> VNode { 79 | /// // Use the `use_context` hook to get access to the context value. 80 | /// let theme = use_context(&THEME_CONTEXT); 81 | /// 82 | /// h!(button) 83 | /// .style( 84 | /// &Style::new() 85 | /// .background_color(match *theme { 86 | /// Theme::LightMode => "white", 87 | /// Theme::DarkMode => "black", 88 | /// }) 89 | /// ) 90 | /// .build("Button") 91 | /// } 92 | /// } 93 | /// ``` 94 | pub fn create_context(init: Rc) -> Context { 95 | Context { 96 | js_context: react_bindings::create_context(RefContainerValue(init)), 97 | phantom: PhantomData, 98 | } 99 | } 100 | 101 | /// A component that can make the given context available for its subtrees. 102 | /// 103 | /// See [`create_context()`] for usage. 104 | #[derive(Debug, Clone)] 105 | pub struct ContextProvider { 106 | context: &'static LocalKey>, 107 | value: Option>, 108 | children: VNode, 109 | } 110 | 111 | impl ContextProvider { 112 | /// Creates a new [`ContextProvider`] from the given context. 113 | pub fn from(context: &'static LocalKey>) -> Self { 114 | Self { 115 | context, 116 | value: None, 117 | children: ().into(), 118 | } 119 | } 120 | 121 | /// Sets the value of the context to be passed down. 122 | pub fn value(mut self, value: Option>) -> Self { 123 | self.value = value; 124 | self 125 | } 126 | 127 | /// Returns a [`VNode`] to be included in a render function. 128 | pub fn build(mut self, children: impl Into) -> VNode { 129 | self.children = children.into(); 130 | Component::build(self) 131 | } 132 | } 133 | 134 | impl Component for ContextProvider { 135 | fn render(&self) -> VNode { 136 | self.context.with(|context| { 137 | create_element( 138 | &Reflect::get(context.as_ref(), &intern("Provider").into()) 139 | .expect_throw("cannot read from context object"), 140 | &{ 141 | let mut props = Props::new(); 142 | 143 | if let Some(value) = self.value.as_ref() { 144 | props = props.insert( 145 | intern("value"), 146 | &RefContainerValue(value.clone()).into(), 147 | ); 148 | } 149 | 150 | props 151 | }, 152 | self.children.clone(), 153 | ) 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/callback.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Ref, RefCell}, 3 | fmt::Debug, 4 | rc::Rc, 5 | }; 6 | use wasm_bindgen::{ 7 | convert::{FromWasmAbi, IntoWasmAbi}, 8 | describe::WasmDescribe, 9 | prelude::Closure, 10 | JsValue, UnwrapThrowExt, 11 | }; 12 | 13 | /// A zero-sized helper struct to simulate a JS-interoperable [`Callback`] with no input 14 | /// arguments. 15 | /// 16 | /// ``` 17 | /// # use wasm_react::*; 18 | /// # fn f() { 19 | /// let cb: Callback = Callback::new(|Void| 5); 20 | /// # } 21 | /// ``` 22 | #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] 23 | pub struct Void; 24 | 25 | impl WasmDescribe for Void { 26 | fn describe() { 27 | JsValue::describe() 28 | } 29 | } 30 | 31 | impl IntoWasmAbi for Void { 32 | type Abi = ::Abi; 33 | 34 | fn into_abi(self) -> Self::Abi { 35 | JsValue::undefined().into_abi() 36 | } 37 | } 38 | 39 | impl FromWasmAbi for Void { 40 | type Abi = ::Abi; 41 | 42 | unsafe fn from_abi(js: Self::Abi) -> Self { 43 | JsValue::from_abi(js); 44 | Void 45 | } 46 | } 47 | 48 | impl From for JsValue { 49 | fn from(_: Void) -> Self { 50 | JsValue::undefined() 51 | } 52 | } 53 | 54 | /// This is a simplified, reference-counted wrapper around an [`FnMut(T) -> U`](FnMut) 55 | /// Rust closure that may be called from JS when `T` and `U` allow. 56 | /// 57 | /// You can also use the [`clones!`](crate::clones!) helper macro to 58 | /// clone-capture the environment more ergonomically. 59 | /// 60 | /// It only supports closures with exactly one input argument and some return 61 | /// value. Memory management is handled by Rust. Whenever Rust drops all clones 62 | /// of the [`Callback`], the closure will be dropped and the function cannot be 63 | /// called from JS anymore. 64 | /// 65 | /// Use [`Void`] to simulate a callback with no arguments. 66 | pub struct Callback { 67 | closure: Rc U>>, 68 | js: Rc U>>>>, 69 | } 70 | 71 | impl Callback 72 | where 73 | T: 'static, 74 | U: 'static, 75 | { 76 | /// Creates a new [`Callback`] from a Rust closure. 77 | pub fn new(f: impl FnMut(T) -> U + 'static) -> Self { 78 | Self { 79 | closure: Rc::new(RefCell::new(f)), 80 | js: Default::default(), 81 | } 82 | } 83 | 84 | /// Returns a Rust closure from the callback. 85 | pub fn to_closure(&self) -> impl FnMut(T) -> U + 'static { 86 | let callback = self.clone(); 87 | move |arg| callback.call(arg) 88 | } 89 | 90 | /// Calls the callback with the given argument. 91 | pub fn call(&self, arg: T) -> U { 92 | let mut f = self.closure.borrow_mut(); 93 | f(arg) 94 | } 95 | 96 | /// Returns a new [`Callback`] by prepending the given closure to the callback. 97 | pub fn premap(&self, mut f: impl FnMut(V) -> T + 'static) -> Callback 98 | where 99 | V: 'static, 100 | { 101 | let cb = self.clone(); 102 | 103 | Callback::new(move |v| { 104 | let t = f(v); 105 | cb.call(t) 106 | }) 107 | } 108 | 109 | /// Returns a new [`Callback`] by appending the given closure to the callback. 110 | pub fn postmap( 111 | &self, 112 | mut f: impl FnMut(U) -> V + 'static, 113 | ) -> Callback 114 | where 115 | V: 'static, 116 | { 117 | let cb = self.clone(); 118 | 119 | Callback::new(move |t| { 120 | let u = cb.call(t); 121 | f(u) 122 | }) 123 | } 124 | 125 | /// Returns a reference to `JsValue` of the callback. 126 | pub fn as_js(&self) -> Ref<'_, JsValue> 127 | where 128 | T: FromWasmAbi, 129 | U: IntoWasmAbi, 130 | { 131 | { 132 | self.js.borrow_mut().get_or_insert_with(|| { 133 | Closure::new({ 134 | let closure = self.closure.clone(); 135 | 136 | move |arg| { 137 | let mut f = closure.borrow_mut(); 138 | f(arg) 139 | } 140 | }) 141 | }); 142 | } 143 | 144 | Ref::map(self.js.borrow(), |x| { 145 | x.as_ref().expect_throw("no closure available").as_ref() 146 | }) 147 | } 148 | } 149 | 150 | impl Callback { 151 | /// Returns a new [`Callback`] that does nothing. 152 | pub fn noop() -> Self { 153 | Callback::new(|_| ()) 154 | } 155 | } 156 | 157 | impl Default for Callback 158 | where 159 | T: 'static, 160 | U: Default + 'static, 161 | { 162 | fn default() -> Self { 163 | Self::new(|_| U::default()) 164 | } 165 | } 166 | 167 | impl From for Callback 168 | where 169 | F: FnMut(T) -> U + 'static, 170 | T: 'static, 171 | U: 'static, 172 | { 173 | fn from(value: F) -> Self { 174 | Self::new(value) 175 | } 176 | } 177 | 178 | impl Debug for Callback { 179 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 180 | f.write_str("Callback(|_| { … })") 181 | } 182 | } 183 | 184 | impl PartialEq for Callback { 185 | fn eq(&self, other: &Self) -> bool { 186 | Rc::ptr_eq(&self.closure, &other.closure) && Rc::ptr_eq(&self.js, &other.js) 187 | } 188 | } 189 | 190 | impl Eq for Callback {} 191 | 192 | impl Clone for Callback { 193 | fn clone(&self) -> Self { 194 | Self { 195 | closure: self.closure.clone(), 196 | js: self.js.clone(), 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/props/h_attrs.rs: -------------------------------------------------------------------------------- 1 | use super::{HtmlTag, H}; 2 | use super::{Props, Style}; 3 | use std::borrow::Cow; 4 | use wasm_bindgen::{intern, JsValue}; 5 | 6 | /// To be used with [`H::dangerously_set_inner_html()`]. 7 | #[derive(Debug, Default, PartialEq, Eq, Clone)] 8 | pub struct DangerousHtml<'a> { 9 | /// The HTML content to be rendered. 10 | pub __html: Cow<'a, str>, 11 | } 12 | 13 | macro_rules! impl_attr { 14 | { $( $attr:ident, $attr_str:literal => $T:ty; )* } => { 15 | $( 16 | #[allow(missing_docs)] 17 | pub fn $attr(self, value: $T) -> Self { 18 | self.attr(intern($attr_str), &Into::into(value)) 19 | } 20 | )* 21 | }; 22 | } 23 | 24 | /// Provides auto-completion for DOM attributes on [`H`]. 25 | impl H> { 26 | /// Equivalent to `props.dangerouslySetInnerHTML = { __html: value.__html };`. 27 | /// 28 | /// See also [React documentation](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html). 29 | /// 30 | /// # Example 31 | /// 32 | /// ``` 33 | /// # use wasm_react::{*, props::*}; 34 | /// fn create_markup() -> DangerousHtml<'static> { 35 | /// DangerousHtml { 36 | /// __html: "First · Second".into() 37 | /// } 38 | /// } 39 | /// 40 | /// # fn f() -> VNode { 41 | /// h!(div) 42 | /// .dangerously_set_inner_html(&create_markup()) 43 | /// .build(()) 44 | /// # } 45 | /// ``` 46 | pub fn dangerously_set_inner_html(self, value: &DangerousHtml) -> Self { 47 | self.attr( 48 | intern("dangerouslySetInnerHTML"), 49 | Props::new() 50 | .insert(intern("__html"), &value.__html[..].into()) 51 | .as_ref(), 52 | ) 53 | } 54 | 55 | /// Overwrites the class name attribute. Use [`h!`](crate::h) for easier way 56 | /// to set the class names. 57 | pub fn class_name(self, value: &str) -> Self { 58 | self.attr(intern("className"), &value.into()) 59 | } 60 | 61 | /// Sets the style attribute. 62 | pub fn style(self, style: &Style) -> Self { 63 | self.attr(intern("style"), style.as_ref()) 64 | } 65 | 66 | impl_attr! { 67 | // Standard HTML Attributes 68 | accesskey, "accessKey" => &str; 69 | contenteditable, "contentEditable" => bool; 70 | contextmenu, "contextMenu" => &str; 71 | dir, "dir" => &str; 72 | draggable, "draggable" => bool; 73 | hidden, "hidden" => bool; 74 | id, "id" => &str; 75 | lang, "lang" => &str; 76 | placeholder, "placeholder" => &str; 77 | slot, "slot" => &str; 78 | spellcheck, "spellCheck" => bool; 79 | tabindex, "tabIndex" => i32; 80 | title, "title" => &str; 81 | translate, "translate" => &str; 82 | radiogroup, "radioGroup" => &str; 83 | 84 | // WAI-ARIA 85 | role, "role" => &str; 86 | 87 | // RDFa Attributes 88 | about, "about" => &str; 89 | datatype, "datatype" => &str; 90 | inlist, "inlist" => impl Into; 91 | prefix, "prefix" => &str; 92 | property, "property" => &str; 93 | resource, "resource" => &str; 94 | vocab, "vocab" => &str; 95 | 96 | // Living Standard 97 | inputmode, "inputMode" => &str; 98 | is, "is" => &str; 99 | 100 | // Standard HTML Attributes 101 | accept, "accept" => &str; 102 | acceptcharset, "acceptCharset" => &str; 103 | action, "action" => &str; 104 | allowfullscreen, "allowFullScreen" => bool; 105 | allowtransparency, "allowTransparency" => bool; 106 | alt, "alt" => &str; 107 | autocomplete, "autoComplete" => &str; 108 | autofocus, "autoFocus" => bool; 109 | autoplay, "autoPlay" => bool; 110 | capture, "capture" => impl Into; 111 | cellpadding, "cellPadding" => impl Into; 112 | cellspacing, "cellSpacing" => impl Into; 113 | challenge, "challenge" => &str; 114 | charset, "charSet" => &str; 115 | checked, "checked" => bool; 116 | cite, "cite" => &str; 117 | classid, "classID" => &str; 118 | cols, "cols" => u32; 119 | colspan, "colSpan" => u32; 120 | content, "content" => &str; 121 | controls, "controls" => bool; 122 | coords, "coords" => &str; 123 | crossorigin, "crossOrigin" => &str; 124 | data, "data" => &str; 125 | datetime, "dateTime" => &str; 126 | default, "default" => bool; 127 | defer, "defer" => bool; 128 | disabled, "disabled" => bool; 129 | download, "download" => impl Into; 130 | enctype, "encType" => &str; 131 | form, "form" => &str; 132 | formaction, "formAction" => &str; 133 | formenctype, "formEncType" => &str; 134 | formmethod, "formMethod" => &str; 135 | formnovalidate, "formNoValidate" => bool; 136 | formtarget, "formTarget" => &str; 137 | frameborder, "frameBorder" => impl Into; 138 | headers, "headers" => &str; 139 | height, "height" => impl Into; 140 | high, "high" => f64; 141 | href, "href" => &str; 142 | hreflang, "hrefLang" => &str; 143 | html_for, "htmlFor" => &str; 144 | html_type, "type" => &str; 145 | httpequiv, "httpEquiv" => &str; 146 | integrity, "integrity" => &str; 147 | keyparams, "keyParams" => &str; 148 | keytype, "keyType" => &str; 149 | kind, "kind" => &str; 150 | label, "label" => &str; 151 | list, "list" => &str; 152 | low, "low" => f64; 153 | manifest, "manifest" => &str; 154 | marginheight, "marginHeight" => f64; 155 | marginwidth, "marginWidth" => f64; 156 | max, "max" => f64; 157 | maxlength, "maxLength" => f64; 158 | media, "media" => &str; 159 | mediagroup, "mediaGroup" => &str; 160 | method, "method" => &str; 161 | min, "min" => impl Into; 162 | minlength, "minLength" => f64; 163 | multiple, "multiple" => bool; 164 | muted, "muted" => bool; 165 | name, "name" => &str; 166 | nonce, "nonce" => &str; 167 | novalidate, "noValidate" => bool; 168 | open, "open" => bool; 169 | optimum, "optimum" => f64; 170 | pattern, "pattern" => &str; 171 | playsinline, "playsInline" => bool; 172 | poster, "poster" => &str; 173 | preload, "preload" => &str; 174 | readonly, "readOnly" => bool; 175 | rel, "rel" => &str; 176 | required, "required" => bool; 177 | reversed, "reversed" => bool; 178 | rows, "rows" => u32; 179 | rowspan, "rowSpan" => u32; 180 | sandbox, "sandbox" => &str; 181 | scope, "scope" => &str; 182 | scoped, "scoped" => bool; 183 | scrolling, "scrolling" => &str; 184 | seamless, "seamless" => bool; 185 | selected, "selected" => bool; 186 | shape, "shape" => &str; 187 | size, "size" => f64; 188 | sizes, "sizes" => &str; 189 | span, "span" => u32; 190 | src, "src" => &str; 191 | srcdoc, "srcDoc" => &str; 192 | srclang, "srcLang" => &str; 193 | srcset, "srcSet" => &str; 194 | start, "start" => f64; 195 | step, "step" => impl Into; 196 | summary, "summary" => &str; 197 | target, "target" => &str; 198 | usemap, "useMap" => &str; 199 | value, "value" => impl Into; 200 | width, "width" => impl Into; 201 | wmode, "wmode" => &str; 202 | wrap, "wrap" => &str; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/component.rs: -------------------------------------------------------------------------------- 1 | use crate::{react_bindings, VNode}; 2 | use std::any::{type_name, Any}; 3 | use js_sys::JsString; 4 | use wasm_bindgen::prelude::*; 5 | 6 | /// Implemented by types which can serve as a [React key][key]. 7 | /// 8 | /// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key 9 | pub trait KeyType: Into {} 10 | 11 | macro_rules! impl_key_type { 12 | { $( $T:ty ),* $( , )? } => { 13 | $( impl KeyType for $T {} )* 14 | }; 15 | } 16 | 17 | impl_key_type! { 18 | &str, String, JsString, 19 | f32, f64, 20 | i8, i16, i32, i64, i128, isize, 21 | u8, u16, u32, u64, u128, usize, 22 | } 23 | 24 | #[doc(hidden)] 25 | pub struct BuildParams { 26 | name: &'static str, 27 | key: Option, 28 | } 29 | 30 | /// Implement this trait on a struct to create a component with the struct as 31 | /// props. 32 | /// 33 | /// The props will be completely controlled by Rust, which makes rendering them 34 | /// relatively simple in Rust. However, since the props struct cannot be 35 | /// constructed in JS, these components cannot be exposed to JS. This means only 36 | /// components written in Rust can render a `Component` by default. 37 | /// 38 | /// See [`export_components!`](crate::export_components!) for how to expose 39 | /// components for JS consumption. 40 | /// 41 | /// # Example 42 | /// 43 | /// ``` 44 | /// # use wasm_react::*; 45 | /// struct Counter(i32); 46 | /// 47 | /// impl Component for Counter { 48 | /// fn render(&self) -> VNode { 49 | /// h!(div).build(("Counter: ", self.0)) 50 | /// } 51 | /// } 52 | /// ``` 53 | pub trait Component: Sized + 'static { 54 | /// The render function. 55 | /// 56 | /// **Do not** call this method in another render function. Instead, use 57 | /// [`Component::build()`] to include your component. 58 | fn render(&self) -> VNode; 59 | 60 | /// Sets the [React key][key]. 61 | /// 62 | /// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key 63 | fn key(self, key: Option) -> Keyed { 64 | Keyed(self, key.map(|x| x.into())) 65 | } 66 | 67 | #[doc(hidden)] 68 | /// Defines parameters for [`Component::build()`]. 69 | fn _build_params(&self) -> BuildParams { 70 | BuildParams { 71 | name: type_name::(), 72 | key: None, 73 | } 74 | } 75 | 76 | #[doc(hidden)] 77 | fn _build_with_name_and_key( 78 | self, 79 | name: &'static str, 80 | key: Option, 81 | ) -> VNode { 82 | VNode::Single(react_bindings::create_rust_component( 83 | name, 84 | &key.unwrap_or(JsValue::UNDEFINED), 85 | ComponentWrapper(Box::new(self)), 86 | )) 87 | } 88 | 89 | /// Returns a [`VNode`] to be included in a render function. 90 | fn build(self) -> VNode { 91 | let BuildParams { name, key } = self._build_params(); 92 | 93 | self._build_with_name_and_key(name, key) 94 | } 95 | 96 | /// Returns a memoized version of your component that skips rendering if props 97 | /// haven't changed. 98 | /// 99 | /// If your component renders the same result given the same props, you can 100 | /// memoize your component for a performance boost. 101 | /// 102 | /// You have to implement [`PartialEq`] on your [`Component`] for this to work. 103 | /// 104 | /// # Example 105 | /// 106 | /// ``` 107 | /// # use std::rc::Rc; 108 | /// # use wasm_react::*; 109 | /// #[derive(PartialEq)] 110 | /// struct MessageBox { 111 | /// message: Rc, 112 | /// } 113 | /// 114 | /// impl Component for MessageBox { 115 | /// fn render(&self) -> VNode { 116 | /// h!(h1[."message-box"]).build(&*self.message) 117 | /// } 118 | /// } 119 | /// 120 | /// struct App; 121 | /// 122 | /// impl Component for App { 123 | /// fn render(&self) -> VNode { 124 | /// h!(div[#"app"]).build( 125 | /// MessageBox { 126 | /// message: Rc::from("Hello World!"), 127 | /// } 128 | /// .memoized() 129 | /// .build() 130 | /// ) 131 | /// } 132 | /// } 133 | /// ``` 134 | fn memoized(self) -> Memoized 135 | where 136 | Self: PartialEq, 137 | { 138 | Memoized(self) 139 | } 140 | } 141 | 142 | /// Wraps your component to assign a [React key][key] to it. 143 | /// 144 | /// See [`Component::key()`]. 145 | /// 146 | /// [key]: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key 147 | #[derive(Debug, PartialEq)] 148 | pub struct Keyed(T, Option); 149 | 150 | impl Component for Keyed { 151 | fn render(&self) -> VNode { 152 | self.0.render() 153 | } 154 | 155 | fn _build_params(&self) -> BuildParams { 156 | let BuildParams { name, .. } = self.0._build_params(); 157 | 158 | BuildParams { 159 | name, 160 | key: self.1.clone(), 161 | } 162 | } 163 | 164 | fn _build_with_name_and_key( 165 | self, 166 | name: &'static str, 167 | key: Option, 168 | ) -> VNode { 169 | self.0._build_with_name_and_key(name, key) 170 | } 171 | } 172 | 173 | /// Wraps your component to let React skip rendering if props haven't changed. 174 | /// 175 | /// See [`Component::memoized()`]. 176 | #[derive(Debug, PartialEq)] 177 | pub struct Memoized(T); 178 | 179 | impl Component for Memoized { 180 | fn render(&self) -> VNode { 181 | self.0.render() 182 | } 183 | 184 | fn _build_params(&self) -> BuildParams { 185 | self.0._build_params() 186 | } 187 | 188 | fn _build_with_name_and_key( 189 | self, 190 | name: &'static str, 191 | key: Option, 192 | ) -> VNode { 193 | VNode::Single(react_bindings::create_rust_memo_component( 194 | name, 195 | &key.unwrap_or(JsValue::UNDEFINED), 196 | MemoComponentWrapper(Box::new(self.0)), 197 | )) 198 | } 199 | } 200 | 201 | trait ObjectSafeComponent { 202 | fn render(&self) -> VNode; 203 | } 204 | 205 | impl ObjectSafeComponent for T { 206 | fn render(&self) -> VNode { 207 | Component::render(self) 208 | } 209 | } 210 | 211 | #[doc(hidden)] 212 | #[wasm_bindgen(js_name = __WasmReact_ComponentWrapper)] 213 | pub struct ComponentWrapper(Box); 214 | 215 | #[wasm_bindgen(js_class = __WasmReact_ComponentWrapper)] 216 | impl ComponentWrapper { 217 | #[wasm_bindgen] 218 | pub fn render(&self) -> JsValue { 219 | self.0.render().into() 220 | } 221 | } 222 | 223 | trait ObjectSafeMemoComponent: ObjectSafeComponent { 224 | fn as_any(&self) -> &dyn Any; 225 | fn eq(&self, other: &dyn Any) -> bool; 226 | } 227 | 228 | impl ObjectSafeMemoComponent for T { 229 | fn as_any(&self) -> &dyn Any { 230 | self 231 | } 232 | 233 | fn eq(&self, other: &dyn Any) -> bool { 234 | other 235 | .downcast_ref::() 236 | .map(|other| PartialEq::eq(self, other)) 237 | .unwrap_or(false) 238 | } 239 | } 240 | 241 | #[doc(hidden)] 242 | #[wasm_bindgen(js_name = __WasmReact_MemoComponentWrapper)] 243 | pub struct MemoComponentWrapper(Box); 244 | 245 | #[wasm_bindgen(js_class = __WasmReact_MemoComponentWrapper)] 246 | impl MemoComponentWrapper { 247 | #[wasm_bindgen] 248 | pub fn render(&self) -> JsValue { 249 | self.0.render().into() 250 | } 251 | 252 | #[wasm_bindgen] 253 | pub fn eq(&self, other: &MemoComponentWrapper) -> bool { 254 | self.0.eq(other.0.as_any()) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/props/h_events.rs: -------------------------------------------------------------------------------- 1 | use super::{HtmlTag, H}; 2 | use wasm_bindgen::intern; 3 | use web_sys::{ 4 | AnimationEvent, DragEvent, Event, FocusEvent, KeyboardEvent, MouseEvent, 5 | PointerEvent, TransitionEvent, UiEvent, WheelEvent, 6 | }; 7 | 8 | use crate::Callback; 9 | 10 | macro_rules! impl_event { 11 | { $( $on_event:ident, $on_event_str:literal => $E:ty; )* } => { 12 | $( 13 | #[allow(missing_docs)] 14 | pub fn $on_event(self, f: &Callback<$E>) -> Self { 15 | self.attr_callback(intern($on_event_str), f) 16 | } 17 | )* 18 | }; 19 | } 20 | 21 | /// Provides auto-completion for DOM events on [`H`]. 22 | impl H> { 23 | impl_event! { 24 | on_focus, "onFocus" => FocusEvent; 25 | on_focus_capture, "onFocusCapture" => FocusEvent; 26 | on_blur, "onBlur" => FocusEvent; 27 | on_blur_capture, "onBlurCapture" => FocusEvent; 28 | 29 | on_change, "onChange" => Event; 30 | on_change_capture, "onChangeCapture" => Event; 31 | on_beforeinput, "onBeforeInput" => Event; 32 | on_beforeinput_capture, "onBeforeInputCapture" => Event; 33 | on_input, "onInput" => Event; 34 | on_input_capture, "onInputCapture" => Event; 35 | on_reset, "onReset" => Event; 36 | on_reset_capture, "onResetCapture" => Event; 37 | on_submit, "onSubmit" => Event; 38 | on_submit_capture, "onSubmitCapture" => Event; 39 | on_invalid, "onInvalid" => Event; 40 | on_invalid_capture, "onInvalidCapture" => Event; 41 | on_select, "onSelect" => UiEvent; 42 | on_select_capture, "onSelectCapture" => UiEvent; 43 | 44 | on_load, "onLoad" => Event; 45 | on_load_capture, "onLoadCapture" => Event; 46 | 47 | on_keydown, "onKeyDown" => KeyboardEvent; 48 | on_keydown_capture, "onKeyDownCapture" => KeyboardEvent; 49 | on_keypress, "onKeyPress" => KeyboardEvent; 50 | on_keypress_capture, "onKeyPressCapture" => KeyboardEvent; 51 | on_keyup, "onKeyUp" => KeyboardEvent; 52 | on_keyup_capture, "onKeyUpCapture" => KeyboardEvent; 53 | 54 | on_abort, "onAbort" => Event; 55 | on_abort_capture, "onAbortCapture" => Event; 56 | on_canplay, "onCanPlay" => Event; 57 | on_canplay_capture, "onCanPlayCapture" => Event; 58 | on_canplay_through, "onCanPlayThrough" => Event; 59 | on_canplay_through_capture, "onCanPlayThroughCapture" => Event; 60 | on_duration_change, "onDurationChange" => Event; 61 | on_duration_change_capture, "onDurationChangeCapture" => Event; 62 | on_emptied, "onEmptied" => Event; 63 | on_emptied_capture, "onEmptiedCapture" => Event; 64 | on_encrypted, "onEncrypted" => Event; 65 | on_encrypted_capture, "onEncryptedCapture" => Event; 66 | on_ended, "onEnded" => Event; 67 | on_ended_capture, "onEndedCapture" => Event; 68 | on_loadeddata, "onLoadedData" => Event; 69 | on_loadeddata_capture, "onLoadedDataCapture" => Event; 70 | on_loadedmetadata, "onLoadedMetadata" => Event; 71 | on_loadedmetadata_capture, "onLoadedMetadataCapture" => Event; 72 | on_loadstart, "onLoadStart" => Event; 73 | on_loadstart_capture, "onLoadStartCapture" => Event; 74 | on_pause, "onPause" => Event; 75 | on_pause_capture, "onPauseCapture" => Event; 76 | on_play, "onPlay" => Event; 77 | on_play_capture, "onPlayCapture" => Event; 78 | on_playing, "onPlaying" => Event; 79 | on_playing_capture, "onPlayingCapture" => Event; 80 | on_progress, "onProgress" => Event; 81 | on_progress_capture, "onProgressCapture" => Event; 82 | on_ratechange, "onRateChange" => Event; 83 | on_ratechange_capture, "onRateChangeCapture" => Event; 84 | on_seeked, "onSeeked" => Event; 85 | on_seeked_capture, "onSeekedCapture" => Event; 86 | on_seeking, "onSeeking" => Event; 87 | on_seeking_capture, "onSeekingCapture" => Event; 88 | on_stalled, "onStalled" => Event; 89 | on_stalled_capture, "onStalledCapture" => Event; 90 | on_suspend, "onSuspend" => Event; 91 | on_suspend_capture, "onSuspendCapture" => Event; 92 | on_timeupdate, "onTimeUpdate" => Event; 93 | on_timeupdate_capture, "onTimeUpdateCapture" => Event; 94 | on_volumechange, "onVolumeChange" => Event; 95 | on_volumechange_capture, "onVolumeChangeCapture" => Event; 96 | on_waiting, "onWaiting" => Event; 97 | on_waiting_capture, "onWaitingCapture" => Event; 98 | 99 | on_auxclick, "onAuxClick" => MouseEvent; 100 | on_auxclick_capture, "onAuxClickCapture" => MouseEvent; 101 | on_click, "onClick" => MouseEvent; 102 | on_click_capture, "onClickCapture" => MouseEvent; 103 | on_context_menu, "onContextMenu" => MouseEvent; 104 | on_context_menu_capture, "onContextMenuCapture" => MouseEvent; 105 | on_doubleclick, "onDoubleClick" => MouseEvent; 106 | on_doubleclick_capture, "onDoubleClickCapture" => MouseEvent; 107 | on_mousedown, "onMouseDown" => MouseEvent; 108 | on_mousedown_capture, "onMouseDownCapture" => MouseEvent; 109 | on_mouseenter, "onMouseEnter" => MouseEvent; 110 | on_mouseleave, "onMouseLeave" => MouseEvent; 111 | on_mousemove, "onMouseMove" => MouseEvent; 112 | on_mousemove_capture, "onMouseMoveCapture" => MouseEvent; 113 | on_mouseout, "onMouseOut" => MouseEvent; 114 | on_mouseout_capture, "onMouseOutCapture" => MouseEvent; 115 | on_mouseover, "onMouseOver" => MouseEvent; 116 | on_mouseover_capture, "onMouseOverCapture" => MouseEvent; 117 | on_mouseup, "onMouseUp" => MouseEvent; 118 | on_mouseup_capture, "onMouseUpCapture" => MouseEvent; 119 | 120 | on_pointerdown, "onPointerDown" => PointerEvent; 121 | on_pointerdown_capture, "onPointerDownCapture" => PointerEvent; 122 | on_pointermove, "onPointerMove" => PointerEvent; 123 | on_pointermove_capture, "onPointerMoveCapture" => PointerEvent; 124 | on_pointerup, "onPointerUp" => PointerEvent; 125 | on_pointerup_capture, "onPointerUpCapture" => PointerEvent; 126 | on_pointercancel, "onPointerCancel" => PointerEvent; 127 | on_pointercancel_capture, "onPointerCancelCapture" => PointerEvent; 128 | on_pointerenter, "onPointerEnter" => PointerEvent; 129 | on_pointerenter_capture, "onPointerEnterCapture" => PointerEvent; 130 | on_pointerleave, "onPointerLeave" => PointerEvent; 131 | on_pointerleave_capture, "onPointerLeaveCapture" => PointerEvent; 132 | on_pointerover, "onPointerOver" => PointerEvent; 133 | on_pointerover_capture, "onPointerOverCapture" => PointerEvent; 134 | on_pointerout, "onPointerOut" => PointerEvent; 135 | on_pointerout_capture, "onPointerOutCapture" => PointerEvent; 136 | on_gotpointer_capture, "onGotPointerCapture" => PointerEvent; 137 | on_gotpointer_capture_capture, "onGotPointerCaptureCapture" => PointerEvent; 138 | on_lostpointer_capture, "onLostPointerCapture" => PointerEvent; 139 | on_lostpointer_capture_capture, "onLostPointerCaptureCapture" => PointerEvent; 140 | 141 | on_drag, "onDrag" => DragEvent; 142 | on_dragcapture, "onDragCapture" => DragEvent; 143 | on_dragend, "onDragEnd" => DragEvent; 144 | on_dragend_capture, "onDragEndCapture" => DragEvent; 145 | on_dragenter, "onDragEnter" => DragEvent; 146 | on_dragenter_capture, "onDragEnterCapture" => DragEvent; 147 | on_dragexit, "onDragExit" => DragEvent; 148 | on_dragexit_capture, "onDragExitCapture" => DragEvent; 149 | on_dragleave, "onDragLeave" => DragEvent; 150 | on_dragleave_capture, "onDragLeaveCapture" => DragEvent; 151 | on_dragover, "onDragOver" => DragEvent; 152 | on_dragover_capture, "onDragOverCapture" => DragEvent; 153 | on_dragstart, "onDragStart" => DragEvent; 154 | on_dragstart_capture, "onDragStartCapture" => DragEvent; 155 | on_drop, "onDrop" => DragEvent; 156 | on_drop_capture, "onDropCapture" => DragEvent; 157 | 158 | on_scroll, "onScroll" => UiEvent; 159 | on_scroll_capture, "onScrollCapture" => UiEvent; 160 | on_wheel, "onWheel" => WheelEvent; 161 | on_wheel_capture, "onWheelCapture" => WheelEvent; 162 | 163 | on_animationstart, "onAnimationStart" => AnimationEvent; 164 | on_animationstart_capture, "onAnimationStartCapture" => AnimationEvent; 165 | on_animationend, "onAnimationEnd" => AnimationEvent; 166 | on_animationend_capture, "onAnimationEndCapture" => AnimationEvent; 167 | on_animationiteration, "onAnimationIteration" => AnimationEvent; 168 | on_animationiteration_capture, "onAnimationIterationCapture" => AnimationEvent; 169 | on_transition_end, "onTransitionEnd" => TransitionEvent; 170 | on_transition_end_capture, "onTransitionEndCapture" => TransitionEvent; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-react 🦀⚛️ 2 | 3 | [![GitHub](https://img.shields.io/badge/GitHub-Repo-lightgrey?logo=github)](https://github.com/yishn/wasm-react) 4 | [![crates.io](https://img.shields.io/crates/v/wasm-react)](https://crates.io/crates/wasm-react) 5 | [![CI](https://github.com/yishn/wasm-react/actions/workflows/ci.yml/badge.svg)](https://github.com/yishn/wasm-react/actions/workflows/ci.yml) 6 | [![docs.rs](https://img.shields.io/docsrs/wasm-react)](https://docs.rs/wasm-react/) 7 | 8 | WASM bindings for [React]. 9 | 10 | ## Introduction 11 | 12 | This library enables you to write and use React components in Rust, which then 13 | can be exported to JS to be reused or rendered. 14 | 15 | ### Why React? 16 | 17 | React is one of the most popular UI framework for JS with a thriving community 18 | and lots of libraries written for it. Standing on the shoulder of giants, you 19 | will be able to write complex frontend applications with Rust. 20 | 21 | ### Goals 22 | 23 | - Provide Rust bindings for the public API of `react` as close to the original 24 | API as possible, but with Rust in mind. 25 | - Provide an ergonomic way to write components. 26 | - Provide ways to interact with components written in JS. 27 | 28 | ### Non-Goals 29 | 30 | - Provide bindings for any other library than `react`, e.g. `react-dom`. 31 | - Reimplementation of the reconciliation algorithm or runtime. 32 | - Emphasis on performance. 33 | 34 | ## Getting Started 35 | 36 | Make sure you have Rust and Cargo installed. You can install `wasm-react` with 37 | cargo. Furthermore, if you want to expose your Rust components to JS, you also 38 | need `wasm-bindgen` and have [`wasm-pack`] installed. 39 | 40 | ```sh 41 | $ cargo add wasm-react 42 | $ cargo add wasm-bindgen@0.2 43 | ``` 44 | 45 | ### Creating a Component 46 | 47 | First, you need to define a struct for the props of your component. To define 48 | the render function, you need to implement the trait `Component` for your 49 | struct: 50 | 51 | ```rust 52 | use wasm_react::{h, Component, VNode}; 53 | 54 | struct Counter { 55 | counter: i32, 56 | } 57 | 58 | impl Component for Counter { 59 | fn render(&self) -> VNode { 60 | h!(div) 61 | .build(( 62 | h!(p).build(("Counter: ", self.counter)), 63 | h!(button).build("Increment"), 64 | )) 65 | } 66 | } 67 | ``` 68 | 69 | ### Add State 70 | 71 | You can use the `use_state()` hook to make your component stateful: 72 | 73 | ```rust 74 | use wasm_react::{h, Component, VNode}; 75 | use wasm_react::hooks::use_state; 76 | 77 | struct Counter { 78 | initial_counter: i32, 79 | } 80 | 81 | impl Component for Counter { 82 | fn render(&self) -> VNode { 83 | let counter = use_state(|| self.initial_counter); 84 | 85 | let result = h!(div) 86 | .build(( 87 | h!(p).build(("Counter: ", *counter.value())), 88 | h!(button).build("Increment"), 89 | )); 90 | result 91 | } 92 | } 93 | ``` 94 | 95 | Note that according to the usual Rust rules, the state will be dropped when the 96 | render function returns. `use_state()` will prevent that by tying the lifetime 97 | of the state to the lifetime of the component, therefore _persisting_ the state 98 | through the entire lifetime of the component. 99 | 100 | ### Add Event Handlers 101 | 102 | To create an event handler, you pass a `Callback` created from a Rust closure. 103 | You can use the helper macro `clones!` to clone-capture the environment more 104 | ergonomically. 105 | 106 | ```rust 107 | use wasm_react::{h, clones, Component, Callback, VNode}; 108 | use wasm_react::hooks::{use_state, Deps}; 109 | 110 | struct Counter { 111 | initial_counter: i32, 112 | } 113 | 114 | impl Component for Counter { 115 | fn render(&self) -> VNode { 116 | let message = use_state(|| "Hello World!"); 117 | let counter = use_state(|| self.initial_counter); 118 | 119 | let result = h!(div) 120 | .build(( 121 | h!(p).build(("Counter: ", *counter.value())), 122 | 123 | h!(button) 124 | .on_click(&Callback::new({ 125 | clones!(message, mut counter); 126 | 127 | move |_| { 128 | println!("{}", message.value()); 129 | counter.set(|c| c + 1); 130 | } 131 | })) 132 | .build("Increment"), 133 | 134 | h!(button) 135 | .on_click(&Callback::new({ 136 | clones!(mut counter); 137 | 138 | move |_| counter.set(|c| c - 1) 139 | })) 140 | .build("Decrement"), 141 | )); 142 | result 143 | } 144 | } 145 | ``` 146 | 147 | ### Export Components for JS Consumption 148 | 149 | First, you'll need [`wasm-pack`]. You can use `export_components!` to export 150 | your Rust component for JS consumption. Requirement is that your component 151 | implements `TryFrom`. 152 | 153 | ```rust 154 | use wasm_react::{h, export_components, Component, VNode}; 155 | use wasm_bindgen::JsValue; 156 | 157 | struct Counter { 158 | initial_counter: i32, 159 | } 160 | 161 | impl Component for Counter { 162 | fn render(&self) -> VNode { 163 | /* … */ 164 | VNode::new() 165 | } 166 | } 167 | 168 | struct App; 169 | 170 | impl Component for App { 171 | fn render(&self) -> VNode { 172 | h!(div).build(( 173 | Counter { 174 | initial_counter: 0, 175 | } 176 | .build(), 177 | )) 178 | } 179 | } 180 | 181 | impl TryFrom for App { 182 | type Error = JsValue; 183 | 184 | fn try_from(_: JsValue) -> Result { 185 | Ok(App) 186 | } 187 | } 188 | 189 | export_components! { App } 190 | ``` 191 | 192 | Use `wasm-pack` to compile your Rust code into WASM: 193 | 194 | ```sh 195 | $ wasm-pack build 196 | ``` 197 | 198 | Depending on your JS project structure, you may want to specify the `--target` 199 | option, see 200 | [`wasm-pack` documentation](https://rustwasm.github.io/docs/wasm-pack/commands/build.html#target). 201 | 202 | Assuming you use a bundler that supports JSX and WASM imports in ES modules like 203 | Webpack, you can use: 204 | 205 | ```js 206 | import React from "react"; 207 | import { createRoot } from "react-dom/client"; 208 | 209 | async function main() { 210 | const { WasmReact, App } = await import("./path/to/pkg/project.js"); 211 | WasmReact.useReact(React); // Tell wasm-react to use your React runtime 212 | 213 | const root = createRoot(document.getElementById("root")); 214 | root.render(); 215 | } 216 | ``` 217 | 218 | If you use plain ES modules, you can do the following: 219 | 220 | ```sh 221 | $ wasm-pack build --target web 222 | ``` 223 | 224 | ```js 225 | import "https://unpkg.com/react/umd/react.production.min.js"; 226 | import "https://unpkg.com/react-dom/umd/react-dom.production.min.js"; 227 | import init, { WasmReact, App } from "./path/to/pkg/project.js"; 228 | 229 | async function main() { 230 | await init(); // Need to load WASM first 231 | WasmReact.useReact(window.React); // Tell wasm-react to use your React runtime 232 | 233 | const root = ReactDOM.createRoot(document.getElementById("root")); 234 | root.render(React.createElement(App, {})); 235 | } 236 | ``` 237 | 238 | ### Import Components for Rust Consumption 239 | 240 | You can use `import_components!` together with `wasm-bindgen` to import JS 241 | components for Rust consumption. First, prepare your JS component: 242 | 243 | ```js 244 | // /.dummy/myComponents.js 245 | import "https://unpkg.com/react/umd/react.production.min.js"; 246 | 247 | export function MyComponent(props) { 248 | /* … */ 249 | } 250 | ``` 251 | 252 | Make sure the component uses the same React runtime as specified for 253 | `wasm-react`. Afterwards, use `import_components!`: 254 | 255 | ```rust 256 | use wasm_react::{h, import_components, Component, VNode}; 257 | use wasm_bindgen::prelude::*; 258 | 259 | import_components! { 260 | #[wasm_bindgen(module = "/.dummy/myComponents.js")] 261 | 262 | MyComponent 263 | } 264 | 265 | struct App; 266 | 267 | impl Component for App { 268 | fn render(&self) -> VNode { 269 | h!(div).build(( 270 | MyComponent::new() 271 | .attr("prop", &"Hello World!".into()) 272 | .build(()), 273 | )) 274 | } 275 | } 276 | ``` 277 | 278 | ### Passing Down Non-Copy Props 279 | 280 | Say you define a component with the following struct: 281 | 282 | ```rust 283 | use std::rc::Rc; 284 | 285 | struct TaskList { 286 | tasks: Vec> 287 | } 288 | ``` 289 | 290 | You want to include `TaskList` in a container component `App` where `tasks` is 291 | managed by a state: 292 | 293 | ```rust 294 | use std::rc::Rc; 295 | use wasm_react::{h, Component, VNode}; 296 | use wasm_react::hooks::{use_state, State}; 297 | 298 | struct TaskList { 299 | tasks: Vec> 300 | } 301 | 302 | impl Component for TaskList { 303 | fn render(&self) -> VNode { 304 | /* … */ 305 | VNode::default() 306 | } 307 | } 308 | 309 | struct App; 310 | 311 | impl Component for App { 312 | fn render(&self) -> VNode { 313 | let tasks: State>> = use_state(|| vec![]); 314 | 315 | h!(div).build(( 316 | TaskList { 317 | tasks: todo!(), // Oops, `tasks.value()` does not fit the type 318 | } 319 | .build(), 320 | )) 321 | } 322 | } 323 | ``` 324 | 325 | Changing the type of `tasks` to fit `tasks.value()` doesn't work, since 326 | `tasks.value()` returns a non-`'static` reference while component structs can 327 | only contain `'static` values. You can clone the underlying `Vec`, but this 328 | introduces unnecessary overhead. In this situation you might think you can 329 | simply change the type of `TaskList` to a `State`: 330 | 331 | ```rust 332 | use std::rc::Rc; 333 | use wasm_react::{h, Component, VNode}; 334 | use wasm_react::hooks::{use_state, State}; 335 | 336 | struct TaskList { 337 | tasks: State>> 338 | } 339 | ``` 340 | 341 | This works as long as the prop `tasks` is guaranteed to come from a state. But 342 | this assumption may not hold. You might want to pass on `Rc>>` or 343 | `Memo>>` instead in the future or somewhere else. To be as generic 344 | as possible, you can use `PropContainer`: 345 | 346 | ```rust 347 | use std::rc::Rc; 348 | use wasm_react::{h, Component, PropContainer, VNode}; 349 | use wasm_react::hooks::{use_state, State}; 350 | 351 | struct TaskList { 352 | tasks: PropContainer>> 353 | } 354 | 355 | impl Component for TaskList { 356 | fn render(&self) -> VNode { 357 | /* Do something with `self.tasks.value()`… */ 358 | VNode::default() 359 | } 360 | } 361 | 362 | struct App; 363 | 364 | impl Component for App { 365 | fn render(&self) -> VNode { 366 | let tasks: State>> = use_state(|| vec![]); 367 | 368 | h!(div).build(( 369 | TaskList { 370 | // Cloning `State` has low cost as opposed to cloning the underlying 371 | // `Vec`. 372 | tasks: tasks.clone().into(), 373 | } 374 | .build(), 375 | )) 376 | } 377 | } 378 | ``` 379 | 380 | ## Known Caveats 381 | 382 | - Rust components cannot be part of the subtree of a `StrictMode` component. 383 | 384 | wasm-react uses React hooks to manually manage Rust memory. `StrictMode` will 385 | run hooks and their destructors twice which will result in a double free. 386 | 387 | ## License 388 | 389 | Licensed under either of 390 | 391 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 392 | ) 393 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or 394 | ) 395 | 396 | at your option. 397 | 398 | ## Contribution 399 | 400 | Unless you explicitly state otherwise, any contribution intentionally submitted 401 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 402 | dual licensed as above, without any additional terms or conditions. 403 | 404 | [react]: https://react.dev 405 | [`wasm-pack`]: https://rustwasm.github.io/wasm-pack/ 406 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Yichuan Shen 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// A convenience macro to [`create_element()`](crate::create_element()) for 2 | /// creating HTML element nodes. 3 | /// 4 | /// Returns an [`H`](crate::props::H) struct that provides 5 | /// auto-completion for HTML attributes and events. 6 | /// 7 | /// # Example 8 | /// 9 | /// ``` 10 | /// # use wasm_react::*; 11 | /// # fn f() -> VNode { 12 | /// h!(div) 13 | /// .attr("id", &"app".into()) 14 | /// .build( 15 | /// h!(h1).build("Hello World!") 16 | /// ) 17 | /// # } 18 | /// 19 | /// //

Hello World!

20 | /// 21 | /// # fn g() -> VNode { 22 | /// h!("web-component") 23 | /// .build("Hello World!") 24 | /// # } 25 | /// 26 | /// // Hello World! 27 | /// ``` 28 | /// 29 | /// It is also possible to add an id and/or classes to the element using a terse 30 | /// notation. You can use the same syntax as [`classnames!`](crate::classnames!). 31 | /// 32 | /// ``` 33 | /// # use wasm_react::*; 34 | /// # fn f() -> VNode { 35 | /// h!(div[#"app"."some-class"."warning"]) 36 | /// .build("This is a warning!") 37 | /// # } 38 | /// 39 | /// //
This is a warning!
40 | /// ``` 41 | #[macro_export] 42 | macro_rules! h { 43 | ($tag:literal $( [$( #$id:literal )? $( .$( $classnames:tt )+ )?] )?) => { 44 | $crate::props::H::new($crate::props::HtmlTag($tag)) $( 45 | $( .id($id) )? 46 | $( .class_name(&$crate::classnames![.$( $classnames )+]) )? 47 | )? 48 | }; 49 | ($tag:ident $( [$( #$id:literal )? $( .$( $classnames:tt )+ )?] )?) => { 50 | $crate::props::H::new($crate::props::HtmlTag(stringify!($tag))) $( 51 | $( .id($id) )? 52 | $( .class_name(&$crate::classnames![.$( $classnames )+]) )? 53 | )? 54 | }; 55 | } 56 | 57 | /// A helper macro which can be used to clone a list of variables. Helpful for 58 | /// creating a closure which clone-captures the environment. 59 | /// 60 | /// # Example 61 | /// 62 | /// ``` 63 | /// # use wasm_react::{*, hooks::*}; 64 | /// # struct C { message: &'static str } 65 | /// # impl C { fn f(&self) { 66 | /// let switch = use_state(|| true); 67 | /// let counter = use_state(|| 0); 68 | /// 69 | /// let cb = Callback::new({ 70 | /// clones!(self.message, switch, mut counter); 71 | /// 72 | /// move |delta: i32| { 73 | /// if (*switch.value()) { 74 | /// println!("{}", message); 75 | /// } 76 | /// 77 | /// counter.set(|c| c + delta); 78 | /// } 79 | /// }); 80 | /// # }} 81 | /// ``` 82 | /// 83 | /// This is equivalent to the following: 84 | /// 85 | /// ``` 86 | /// # use wasm_react::{*, hooks::*}; 87 | /// # struct C { message: &'static str } 88 | /// # impl C { fn f(&self) { 89 | /// let switch = use_state(|| true); 90 | /// let counter = use_state(|| 0); 91 | /// 92 | /// let cb = Callback::new({ 93 | /// let message = self.message.clone(); 94 | /// let switch = switch.clone(); 95 | /// let mut counter = counter.clone(); 96 | /// 97 | /// move |delta: i32| { 98 | /// if (*switch.value()) { 99 | /// println!("{}", message); 100 | /// } 101 | /// 102 | /// counter.set(|c| c + delta); 103 | /// } 104 | /// }); 105 | /// # }} 106 | /// ``` 107 | #[macro_export] 108 | macro_rules! clones { 109 | (@clones $(,)? mut $obj:ident.$id:ident $( $tail:tt )*) => { 110 | let mut $id = $obj.$id.clone(); 111 | $crate::clones!(@clones $( $tail )*); 112 | }; 113 | (@clones $(,)? $obj:ident.$id:ident $( $tail:tt )*) => { 114 | let $id = $obj.$id.clone(); 115 | $crate::clones!(@clones $( $tail )*); 116 | }; 117 | (@clones $(,)? mut $id:ident $( $tail:tt )*) => { 118 | let mut $id = $id.clone(); 119 | $crate::clones!(@clones $( $tail )*); 120 | }; 121 | (@clones $(,)? $id:ident $( $tail:tt )*) => { 122 | let $id = $id.clone(); 123 | $crate::clones!(@clones $( $tail )*); 124 | }; 125 | (@clones) => {}; 126 | 127 | ($( $tt:tt )*) => { 128 | $crate::clones!(@clones $( $tt )*); 129 | }; 130 | } 131 | 132 | /// Constructs a [`String`] based on various types that implement 133 | /// [`Classnames`](crate::props::Classnames). 134 | /// 135 | /// # Example 136 | /// 137 | /// ``` 138 | /// # use wasm_react::*; 139 | /// assert_eq!( 140 | /// classnames![."button"."blue"], 141 | /// "button blue ".to_string(), 142 | /// ); 143 | /// 144 | /// let blue = false; 145 | /// let disabled = true; 146 | /// 147 | /// assert_eq!( 148 | /// classnames![."button".blue.disabled], 149 | /// "button disabled ".to_string(), 150 | /// ); 151 | /// 152 | /// let is_blue = Some("blue"); 153 | /// let disabled = "disabled".to_string(); 154 | /// 155 | /// assert_eq!( 156 | /// classnames![."button".{is_blue}.{disabled}], 157 | /// "button blue disabled ", 158 | /// ); 159 | /// ``` 160 | #[macro_export] 161 | macro_rules! classnames { 162 | [@single $result:ident <<] => {}; 163 | 164 | // Handle string literals 165 | [@single $result:ident << .$str:literal $( $tail:tt )*] => { 166 | $crate::props::Classnames::append_to(&$str, &mut $result); 167 | $crate::classnames![@single $result << $( $tail ) *]; 168 | }; 169 | 170 | // Handle boolean variables 171 | [@single $result:ident << .$bool:ident $( $tail:tt )*] => { 172 | $crate::props::Classnames::append_to( 173 | &$bool.then(|| stringify!($bool)), 174 | &mut $result 175 | ); 176 | $crate::classnames![@single $result << $( $tail ) *]; 177 | }; 178 | 179 | // Handle block expressions 180 | [@single $result:ident << .$block:block $( $tail:tt )*] => { 181 | $crate::props::Classnames::append_to(&$block, &mut $result); 182 | $crate::classnames![@single $result << $( $tail ) *]; 183 | }; 184 | 185 | [] => { 186 | ::std::string::String::new() 187 | }; 188 | [$( $tt:tt )*] => { 189 | { 190 | let mut result = ::std::string::String::new(); 191 | $crate::classnames![@single result << $( $tt )*]; 192 | result 193 | } 194 | }; 195 | } 196 | 197 | /// This macro can be used to expose your [`Component`](crate::Component) for JS 198 | /// consumption via `wasm-bindgen`. 199 | /// 200 | /// Requirement is that you implement the [`TryFrom`](core::convert::TryFrom) 201 | /// trait on your component and that you do not export anything else that has 202 | /// the same name as your component. 203 | /// 204 | /// Therefore, it is only recommended to use this macro if you're writing a 205 | /// library for JS consumption only, or if you're writing a standalone 206 | /// application, since this will pollute the export namespace, which isn't 207 | /// desirable if you're writing a library for Rust consumption only. 208 | /// 209 | /// # Example 210 | /// 211 | /// Implement [`TryFrom`](core::convert::TryFrom) on 212 | /// your component and export it: 213 | /// 214 | /// ``` 215 | /// # use wasm_react::*; 216 | /// # use wasm_bindgen::prelude::*; 217 | /// # use js_sys::Reflect; 218 | /// # 219 | /// pub struct Counter { 220 | /// counter: i32, 221 | /// } 222 | /// 223 | /// impl Component for Counter { 224 | /// # fn render(&self) -> VNode { VNode::new() } 225 | /// /* … */ 226 | /// } 227 | /// 228 | /// impl TryFrom for Counter { 229 | /// type Error = JsValue; 230 | /// 231 | /// fn try_from(value: JsValue) -> Result { 232 | /// let diff = Reflect::get(&value, &"counter".into())? 233 | /// .as_f64() 234 | /// .ok_or(JsError::new("`counter` property not found"))?; 235 | /// 236 | /// Ok(Counter { counter: diff as i32 }) 237 | /// } 238 | /// } 239 | /// 240 | /// export_components! { Counter } 241 | /// ``` 242 | /// 243 | /// In JS, you can use it like any other component: 244 | /// 245 | /// ```js 246 | /// import React from "react"; 247 | /// import init, { Counter } from "./path/to/pkg/project.js"; 248 | /// 249 | /// function SomeOtherJsComponent(props) { 250 | /// return ( 251 | ///
252 | /// 253 | ///
254 | /// ); 255 | /// } 256 | /// ``` 257 | /// 258 | /// You can export multiple components and also rename them: 259 | /// 260 | /// ``` 261 | /// # use wasm_react::*; 262 | /// # use wasm_bindgen::prelude::*; 263 | /// # pub struct App; pub struct Counter; 264 | /// # impl Component for App { fn render(&self) -> VNode { VNode::new() } } 265 | /// # impl TryFrom for App { 266 | /// # type Error = JsValue; 267 | /// # fn try_from(_: JsValue) -> Result { todo!() } 268 | /// # } 269 | /// # impl Component for Counter { fn render(&self) -> VNode { VNode::new() } } 270 | /// # impl TryFrom for Counter { 271 | /// # type Error = JsValue; 272 | /// # fn try_from(_: JsValue) -> Result { todo!() } 273 | /// # } 274 | /// export_components! { 275 | /// /// Some doc comment for the exported component. 276 | /// App as CounterApp, 277 | /// Counter 278 | /// } 279 | /// ``` 280 | #[macro_export] 281 | macro_rules! export_components { 282 | {} => {}; 283 | { 284 | $( #[$meta:meta] )* 285 | $Component:ident $( , $( $tail:tt )* )? 286 | } => { 287 | $crate::export_components! { 288 | $( #[$meta] )* 289 | $Component as $Component $( , $( $tail )* )? 290 | } 291 | }; 292 | { 293 | $( #[$meta:meta] )* 294 | $Component:ty as $Name:ident $( , $( $tail:tt )* )? 295 | } => { 296 | $crate::paste! { 297 | $( #[$meta] )* 298 | #[allow(non_snake_case)] 299 | #[allow(dead_code)] 300 | #[doc(hidden)] 301 | #[::wasm_bindgen::prelude::wasm_bindgen(js_name = $Name)] 302 | pub fn [<__WasmReact_Export_ $Name>]( 303 | props: ::wasm_bindgen::JsValue, 304 | ) -> ::wasm_bindgen::JsValue 305 | where 306 | $Component: $crate::Component 307 | + TryFrom<::wasm_bindgen::JsValue, Error = ::wasm_bindgen::JsValue> 308 | { 309 | let component_ref = $crate::hooks::use_memo({ 310 | let props = props.clone(); 311 | 312 | move || $Component::try_from(props).unwrap() 313 | }, $crate::hooks::Deps::some(props)); 314 | 315 | $crate::react_bindings::use_rust_tmp_refs(); 316 | 317 | let component = component_ref.value(); 318 | $crate::Component::render(&*component).into() 319 | } 320 | } 321 | 322 | $( $crate::export_components! { $( $tail )* } )? 323 | }; 324 | } 325 | 326 | /// This macro can be used to import JS React components for Rust consumption 327 | /// via `wasm-bindgen`. 328 | /// 329 | /// Make sure that the components you import use the same React runtime as 330 | /// specified for `wasm-react`. 331 | /// 332 | /// # Example 333 | /// 334 | /// Assume the JS components are defined and exported in `/.dummy/myComponents.js`: 335 | /// 336 | /// ```js 337 | /// import "https://unpkg.com/react/umd/react.production.min.js"; 338 | /// 339 | /// export function MyComponent(props) { /* … */ } 340 | /// export function PublicComponent(props) { /* … */ } 341 | /// export function RenamedComponent(props) { /* … */ } 342 | /// ``` 343 | /// 344 | /// Then you can import them using `import_components!`: 345 | /// 346 | /// ``` 347 | /// # use wasm_react::*; 348 | /// # use wasm_bindgen::prelude::*; 349 | /// import_components! { 350 | /// #[wasm_bindgen(module = "/.dummy/myComponents.js")] 351 | /// 352 | /// /// Some doc comment for the imported component. 353 | /// MyComponent, 354 | /// /// This imported component will be made public. 355 | /// pub PublicComponent, 356 | /// /// You can rename imported components. 357 | /// RenamedComponent as pub OtherComponent, 358 | /// } 359 | /// ``` 360 | /// 361 | /// Now you can include the imported components in your render function: 362 | /// 363 | /// ``` 364 | /// # use wasm_react::{*, props::*}; 365 | /// # use wasm_bindgen::prelude::*; 366 | /// # import_components! { #[wasm_bindgen(inline_js = "")] MyComponent } 367 | /// # struct App; 368 | /// # impl Component for App { 369 | /// fn render(&self) -> VNode { 370 | /// h!(div).build( 371 | /// MyComponent::new() 372 | /// .attr("prop", &"Hello World!".into()) 373 | /// .build(()) 374 | /// ) 375 | /// } 376 | /// # } 377 | /// ``` 378 | /// 379 | /// # Defining Custom Convenience Methods 380 | /// 381 | /// `MyComponent::new()` returns an [`H`](crate::props::H) which 382 | /// can be used to define convenience methods by using a new extension trait: 383 | /// 384 | /// ``` 385 | /// # use wasm_react::{*, props::*}; 386 | /// # use wasm_bindgen::prelude::*; 387 | /// # import_components! { #[wasm_bindgen(inline_js = "")] MyComponent } 388 | /// trait HMyComponentExt { 389 | /// fn prop(self, value: &str) -> Self; 390 | /// } 391 | /// 392 | /// impl HMyComponentExt for H { 393 | /// fn prop(self, value: &str) -> Self { 394 | /// self.attr("prop", &value.into()) 395 | /// } 396 | /// } 397 | /// 398 | /// /* … */ 399 | /// 400 | /// # struct App; 401 | /// # impl Component for App { 402 | /// fn render(&self) -> VNode { 403 | /// h!(div).build( 404 | /// MyComponent::new() 405 | /// .prop("Hello World!") 406 | /// .build(()) 407 | /// ) 408 | /// } 409 | /// # } 410 | /// ``` 411 | #[macro_export] 412 | macro_rules! import_components { 413 | { #[$from:meta] } => {}; 414 | { 415 | #[$from:meta] 416 | $( #[$meta:meta] )* 417 | $vis:vis $Component:ident $( , $( $tail:tt )* )? 418 | } => { 419 | $crate::import_components! { 420 | #[$from] 421 | $( #[$meta] )* 422 | $Component as $vis $Component $( , $( $tail )* )? 423 | } 424 | }; 425 | { 426 | #[$from:meta] 427 | $( #[$meta:meta] )* 428 | $Component:ident as $vis:vis $Name:ident $( , $( $tail:tt )* )? 429 | } => { 430 | $crate::paste! { 431 | #[$from] 432 | extern "C" { 433 | #[wasm_bindgen::prelude::wasm_bindgen(js_name = $Component)] 434 | static [<__WASMREACT_IMPORT_ $Name:upper>]: wasm_bindgen::JsValue; 435 | } 436 | 437 | $( #[$meta] )* 438 | #[derive(Debug, Clone, Copy)] 439 | $vis struct $Name; 440 | 441 | impl $Name { 442 | #[doc = "Returns an `H<" $Name ">` struct that provides convenience " 443 | "methods for adding props."] 444 | pub fn new() -> $crate::props::H<$Name> { 445 | $crate::props::H::new($Name) 446 | } 447 | } 448 | 449 | impl $crate::props::HType for $Name { 450 | fn as_js(&self) -> std::borrow::Cow<'_, JsValue> { 451 | std::borrow::Cow::Borrowed(&[<__WASMREACT_IMPORT_ $Name:upper>]) 452 | } 453 | } 454 | } 455 | 456 | $( $crate::import_components! { #[$from] $( $tail )* } )? 457 | }; 458 | } 459 | -------------------------------------------------------------------------------- /src/props/style.rs: -------------------------------------------------------------------------------- 1 | use super::Props; 2 | use wasm_bindgen::{intern, JsValue}; 3 | 4 | /// A convenience wrapper around [`Props`] that provides auto-completion for 5 | /// style-related properties. 6 | /// 7 | /// # Example 8 | /// 9 | /// ``` 10 | /// # use wasm_react::props::*; 11 | /// # fn f() -> Style { 12 | /// Style::new() 13 | /// .display("grid") 14 | /// .grid("1fr 1fr / 1fr 1fr") 15 | /// # } 16 | /// ``` 17 | #[derive(Debug, Default, Clone)] 18 | pub struct Style(Props); 19 | 20 | impl Style { 21 | /// Creates a new, empty object. 22 | pub fn new() -> Self { 23 | Self(Props::new()) 24 | } 25 | 26 | /// Equivalent to `props[key] = value;`. 27 | pub fn insert(self, key: &str, value: &JsValue) -> Self { 28 | Self(self.0.insert(key, value)) 29 | } 30 | } 31 | 32 | impl AsRef for Style { 33 | fn as_ref(&self) -> &JsValue { 34 | self.0.as_ref() 35 | } 36 | } 37 | 38 | impl From