@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 +52 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/build.rs +59 -0
- package/package.json +32 -0
- package/src/dom.rs +542 -0
- package/src/elements.rs +8556 -0
- package/src/event.rs +238 -0
- package/src/lib.rs +1065 -0
- package/versions.json +2103 -0
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
|
+
}
|