@rust-js/react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml ADDED
@@ -0,0 +1,52 @@
1
+ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
2
+ #
3
+ # When uploading crates to the registry Cargo will automatically
4
+ # "normalize" Cargo.toml files for maximal compatibility
5
+ # with all versions of Cargo and also rewrite `path` dependencies
6
+ # to registry (e.g., crates.io) dependencies.
7
+ #
8
+ # If you are reading this file be aware that the original Cargo.toml
9
+ # will likely look very different (and much more reasonable).
10
+ # See Cargo.toml.orig for the original contents.
11
+
12
+ [package]
13
+ edition = "2024"
14
+ name = "rust-js-react"
15
+ version = "0.0.1"
16
+ build = "build.rs"
17
+ include = [
18
+ "src",
19
+ "build.rs",
20
+ "versions.json",
21
+ "README.md",
22
+ ]
23
+ autolib = false
24
+ autobins = false
25
+ autoexamples = false
26
+ autotests = false
27
+ autobenches = false
28
+ description = "React for rust-js: components written in Rust, compiled to the JSX you'd write"
29
+ readme = "README.md"
30
+ license = "MIT"
31
+ repository = "https://github.com/rust-js-lang/rust-js"
32
+
33
+ [package.metadata.rust-js.npm]
34
+ react = ">=18.0.0"
35
+ react-dom = ">=18.0.0"
36
+
37
+ [lib]
38
+ name = "react"
39
+ path = "src/lib.rs"
40
+
41
+ [dependencies.js]
42
+ version = "~0.0.1"
43
+ package = "rust-js-builtins"
44
+
45
+ [dependencies.webapi]
46
+ version = "~0.0.1"
47
+ package = "rust-js-webapi"
48
+
49
+ [lints.rust.unexpected_cfgs]
50
+ level = "warn"
51
+ priority = 0
52
+ check-cfg = ["cfg(rust_js)"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tu Nguyen
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.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # react: React for rust-js
2
+
3
+ Write React components in Rust. rust-js compiles them to the components
4
+ you'd write in JSX by hand ([ADR 0041](../docs/decisions/0041-react.md)).
5
+ The crate binds all of React's and React DOM's API, as of React 19.3, for
6
+ whichever React from 18.0 on your project has installed
7
+ ([ADR 0043](../docs/decisions/0043-react-versions.md)):
8
+
9
+ ```rust
10
+ #![allow(non_snake_case)]
11
+
12
+ use react::{Element, jsx, use_state};
13
+
14
+ pub fn App() -> Element {
15
+ let (count, set_count) = use_state(0);
16
+ jsx! {
17
+ <button className="counter" onClick={move |_| set_count.update(|count| count + 1)}>
18
+ {"Count is "}{count}
19
+ </button>
20
+ }
21
+ }
22
+ ```
23
+
24
+ ```jsx
25
+ // App.jsx
26
+ import { useState } from "react";
27
+
28
+ export function App() {
29
+ const [count, setCount] = useState(0);
30
+ return <button className="counter" onClick={() => setCount((count) => count + 1 | 0)}>Count is {count}</button>;
31
+ }
32
+ ```
33
+
34
+ It's all bindings: rustc checks the types, and nothing of this crate ends up
35
+ in the JS.
36
+
37
+ `jsx!` is built into rust-js, in both the native and browser compilers, and
38
+ imported as any macro is, `use react::jsx;`.
39
+ Tags and attributes look like JSX; expressions inside braces are Rust.
40
+ Text is quoted (`{"Hello"}`), and handlers are Rust closures. See the
41
+ [syntax guide](../docs/jsx.md) for components, spreads and current limits.
42
+
43
+ **Use JSX to construct elements.** Element builders and `component(...)` are
44
+ compiler plumbing; direct use produces an error. Hooks, styles and React DOM
45
+ operations remain ordinary Rust APIs.
46
+
47
+ Attributes use React's spelling: `className`, `onPointerDownCapture`,
48
+ `aria-label`, and so on. Handlers receive typed events such as
49
+ `event::Mouse` and `event::Keyboard`. A `ref` accepts a ref object or a
50
+ callback; `action` and `formAction` accept URLs, or on React 19+, functions
51
+ and action dispatches. No alternate prop names are needed.
52
+
53
+ A component is a function with a capitalized name that returns `Element`.
54
+ Its props, if it has any, are a struct, and a field named `children` holds
55
+ its children. A tuple of children is several of them, a `Vec` is a list
56
+ whose items need keys, and `None` renders nothing.
57
+
58
+ With `js::camel_case!();` in the crate root, props and custom hooks get
59
+ React's names: a field `on_press` is the prop `onPress`, and `pub fn use_clicks()`
60
+ is the hook `useClicks`, which React's rules and React Compiler find by that
61
+ name ([ADR 0046](../docs/decisions/0046-camel-case-crates.md)).
62
+
63
+ **Hooks** are React's, in snake case, with a `_with` variant for an optional
64
+ argument: `use_state`, `use_reducer`, `use_context`, `use_ref`,
65
+ `use_imperative_handle`, `use_effect`, `use_layout_effect`,
66
+ `use_insertion_effect`, `use_effect_event`, `use_memo`, `use_callback`,
67
+ `use_transition`, `use_deferred_value`, `use_id`, `use_sync_external_store`,
68
+ `use_debug_value`, `use_action_state`, `use_optimistic`, and React 19's
69
+ `use` as `use_`, since `use` is a Rust keyword.
70
+
71
+ - What a hook gives back is `&'static T`, read-only as React's state is, and
72
+ `Copy`, so every handler can `move` it in.
73
+ - An effect's dependencies are a tuple: `(count, name)` is `[count, name]`,
74
+ and `()` is `[]`.
75
+ - An effect's closure returns nothing, or the closure that cleans it up.
76
+
77
+ **Context and `memo`** are made once, as JS makes them at a module's top
78
+ level. In Rust that's a `thread_local!`, which rust-js compiles to just that:
79
+
80
+ ```rust
81
+ thread_local! {
82
+ static THEME: Context<&'static str> = create_context("light");
83
+ static FAST_CARD: Memo<CardProps> = memo(Card);
84
+ }
85
+
86
+ pub fn Toolbar() -> Element {
87
+ let theme = use_context(&THEME);
88
+ jsx! { <FAST_CARD title={theme} /> }
89
+ }
90
+
91
+ pub fn App() -> Element {
92
+ jsx! { <THEME value="dark"><Toolbar /></THEME> }
93
+ }
94
+ ```
95
+
96
+ ```jsx
97
+ const THEME = createContext("light");
98
+ const FAST_CARD = memo(Card);
99
+
100
+ export function Toolbar() {
101
+ const theme = useContext(THEME);
102
+ return <FAST_CARD title={theme} />;
103
+ }
104
+
105
+ export function App() {
106
+ return <THEME value="dark">
107
+ <Toolbar />
108
+ </THEME>;
109
+ }
110
+ ```
111
+
112
+ `memo_with(Card, |a, b| ..)` is `memo(Card, arePropsEqual)`.
113
+ Use `<THEME.Provider value="dark">...</THEME.Provider>` on React 18;
114
+ React 19 also supports `<THEME value="dark">...</THEME>`.
115
+
116
+ Put a context in a module of its own, `mod theme;`, as React advises. Saving
117
+ a file runs its module again, and a context made there is a new one, so
118
+ React remounts everything under its provider and its state is lost. That's
119
+ the same in hand-written React. rust-js leaves an unchanged `theme.js` alone
120
+ when `App.rs` changes, so Fast Refresh keeps the state. `memo` needs nothing
121
+ like this.
122
+
123
+ **React DOM** is `react::dom`: `create_portal`, `flush_sync`, the resource
124
+ hints (`preload`, `preinit`, ..), `use_form_status`, `request_form_reset`
125
+ and `browser`. Under it:
126
+ - `dom::client`: `create_root` and `hydrate_root`, with `RootOptions`;
127
+ - `dom::server`: `render_to_string`, `render_to_readable_stream`,
128
+ `render_to_pipeable_stream`, `resume`, ..;
129
+ - `dom::prerender`: `react-dom/static`'s `prerender` and the rest.
130
+
131
+ Options are objects built by methods:
132
+ `create_root_with(el, RootOptions::new().identifier_prefix("app-"))` is
133
+ `createRoot(el, { identifierPrefix: "app-" })`.
134
+
135
+ Left out on purpose:
136
+ - class components and error boundaries, since rust-js has no classes;
137
+ - React's other legacy APIs (`createElement`, `cloneElement`, `Children`,
138
+ `createRef`, `isValidElement`);
139
+ - anything `unstable_`.
140
+
141
+ ## React versions
142
+
143
+ What a React release after 18.0 added is gated by it, `#[cfg(react = "19.2")]`,
144
+ and the crate is built for the React your project has. On React 18.2,
145
+ `use_effect_event` doesn't compile, and the error says why:
146
+
147
+ ```text
148
+ error[E0432]: unresolved import `react::use_effect_event`
149
+ note: found an item that was configured out
150
+ 496 | #[cfg(react = "19.2")]
151
+ | -------------- the item is gated behind the `19.2` feature
152
+ ```
153
+
154
+ [`versions.json`](versions.json) records which release first has each export,
155
+ event and attribute, read from the releases themselves by
156
+ [`generate.ts`](generate.ts). The tests check every binding against it, for
157
+ every release. When React has a new release, `bun run generate:react` reads
158
+ it. Then the tests say which of its exports still need binding, and with
159
+ which gate.
160
+
161
+ ## Build it
162
+
163
+ ```bash
164
+ react/build.sh -o target/libreact.rmeta # the latest React; also writes libwebapi.rmeta and libjs.rmeta
165
+ react/build.sh -o target/react-18/libreact.rmeta --react 18.2.0 # for React 18.2
166
+ rust-js App.rs -- --extern react=target/libreact.rmeta -L target
167
+ ```
168
+
169
+ `bun run build` does the first step. In a Vite project,
170
+ [@rust-js/vite-plugin](../vite-plugin/index.js) does both, on every save, for
171
+ the React the project has installed. See
172
+ [examples/vite-react](../examples/vite-react/README.md).
package/build.rs ADDED
@@ -0,0 +1,59 @@
1
+ //! The react crate's `cfg`s for one React release (ADR 0043), as `cfg.js`
2
+ //! gives `react/build.sh` them: `react="18.1"` and so on for every minor
3
+ //! release up to `RUST_JS_REACT`, so an item gated `#[cfg(react = "19.2")]`
4
+ //! exists only from React 19.2. The releases are `versions.json`'s.
5
+
6
+ use std::path::Path;
7
+
8
+ fn main() {
9
+ let versions = Path::new(env!("CARGO_MANIFEST_DIR")).join("versions.json");
10
+ println!("cargo::rerun-if-changed={}", versions.display());
11
+ println!("cargo::rerun-if-env-changed=RUST_JS_REACT");
12
+ let text = std::fs::read_to_string(&versions).expect("react/versions.json");
13
+ let releases = releases(&text);
14
+ let latest = *releases.last().expect("react/versions.json lists no release");
15
+ let wanted = match std::env::var("RUST_JS_REACT") {
16
+ Ok(version) => minor(&version).unwrap_or_else(|| panic!("RUST_JS_REACT: not a React version: {version}")),
17
+ Err(_) => minor(latest).expect("a release is a version"),
18
+ };
19
+ let first = minor(releases[0]).expect("a release is a version");
20
+ assert!(
21
+ first <= wanted,
22
+ "React {}.{} is older than {}, the first release the react crate supports",
23
+ wanted.0,
24
+ wanted.1,
25
+ releases[0]
26
+ );
27
+ if wanted > minor(latest).expect("a release is a version") {
28
+ println!(
29
+ "cargo::warning=React {}.{} is newer than {latest}, the latest release the react crate knows: it has {latest}'s API",
30
+ wanted.0, wanted.1
31
+ );
32
+ }
33
+ let values: Vec<String> = releases.iter().map(|release| format!("\"{release}\"")).collect();
34
+ println!("cargo::rustc-check-cfg=cfg(react,values({}))", values.join(","));
35
+ for release in releases
36
+ .iter()
37
+ .filter(|release| minor(release).is_some_and(|r| r <= wanted))
38
+ {
39
+ println!("cargo::rustc-cfg=react=\"{release}\"");
40
+ }
41
+ }
42
+
43
+ /// `versions.json`'s releases, `"18.0"` to the latest, as react/generate.ts
44
+ /// writes them: the keys of its `"releases"` object, one to a line.
45
+ fn releases(text: &str) -> Vec<&str> {
46
+ let start = text.find("\"releases\": {").expect("versions.json has releases");
47
+ let end = start + text[start..].find('}').expect("releases end");
48
+ text[start..end]
49
+ .lines()
50
+ .skip(1)
51
+ .filter_map(|line| line.trim().strip_prefix('"')?.split_once('"').map(|(key, _)| key))
52
+ .collect()
53
+ }
54
+
55
+ /// A version's major and minor release: `18.2.0` is `(18, 2)`.
56
+ fn minor(version: &str) -> Option<(u32, u32)> {
57
+ let mut parts = version.split('.');
58
+ Some((parts.next()?.parse().ok()?, parts.next()?.parse().ok()?))
59
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@rust-js/react",
3
+ "version": "0.0.1",
4
+ "description": "React for rust-js: components written in Rust, compiled to the JSX you'd write",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/rust-js-lang/rust-js.git",
9
+ "directory": "react"
10
+ },
11
+ "keywords": [
12
+ "rust-js",
13
+ "rust"
14
+ ],
15
+ "rust-js": {
16
+ "crate": "rust-js-react"
17
+ },
18
+ "peerDependencies": {
19
+ "@rust-js/builtins": "~0.0.1",
20
+ "@rust-js/webapi": "~0.0.1",
21
+ "react": ">=18.0.0",
22
+ "react-dom": ">=18.0.0"
23
+ },
24
+ "files": [
25
+ "Cargo.toml",
26
+ "LICENSE",
27
+ "README.md",
28
+ "build.rs",
29
+ "src",
30
+ "versions.json"
31
+ ]
32
+ }