Skip to main content

autumn_plugin_react/
lib.rs

1//! React islands for Autumn apps: React 19 components inside Maud + htmx
2//! pages.
3//!
4//! 1. Build your components with a bundler. The entry file registers them
5//!    (the README has a version that also checks the global):
6//!
7//!    ```js
8//!    import { createElement, version } from "react";
9//!    import { flushSync } from "react-dom";
10//!    import { createRoot } from "react-dom/client";
11//!    import { Counter } from "./Counter.jsx";
12//!
13//!    (window.autumnReact ??= []).push({
14//!      createElement, createRoot, flushSync, version, components: { Counter },
15//!    });
16//!    ```
17//!
18//! 2. Embed the build output as a `PluginAssets` bundle and install it:
19//!
20//! ```rust,no_run
21//! use autumn_plugin_react::{Island, ReactPlugin, react_bundle, react_script};
22//! use autumn_web::assets::PluginAssets;
23//! use autumn_web::prelude::*;
24//!
25//! static ISLANDS: PluginAssets = PluginAssets::from_files(
26//!     "app-islands",
27//!     &[("islands.js", b"/* esbuild output */")],
28//! );
29//!
30//! #[get("/")]
31//! async fn index() -> AutumnResult<Markup> {
32//!     let counter = Island::new("Counter")
33//!         .props(&serde_json::json!({ "start": 3 }))?
34//!         .fallback(html! { p { "Count: 3" } });
35//!     Ok(html! {
36//!         html {
37//!             head { (react_script()) (react_bundle(&ISLANDS)) }
38//!             body { (counter) }
39//!         }
40//!     })
41//! }
42//!
43//! # async fn run() {
44//! autumn_web::app()
45//!     .plugin(ReactPlugin::new().bundle(&ISLANDS))
46//!     .routes(routes![index])
47//!     .run()
48//!     .await;
49//! # }
50//! ```
51//!
52//! The loader (`react-islands.js`) mounts each island with `createRoot`. It
53//! mounts islands that htmx swaps in and unmounts islands that htmx swaps
54//! out. A `data-react-props` change renders the same root again, so React
55//! keeps the state. It uses no inline script and no `eval`, so it works
56//! under the default CSP (`script-src 'self'`).
57//!
58//! # Security
59//!
60//! The loader mounts each `data-react-island` element in the page. If your
61//! app shows user HTML, the sanitizer must remove `data-react-*`, `hx-*`
62//! and `data-hx-*` attributes. `data-react-ignore` alone is not sufficient:
63//! an htmx out-of-band swap can move an element out of it. Do not spread
64//! untrusted props onto DOM elements.
65
66mod assets;
67mod island;
68mod plugin;
69mod tags;
70mod update;
71
72pub use assets::{ASSETS_NAMESPACE, LOADER_JS, REACT_ASSETS};
73pub use island::{AttrError, Island, JsonKind, MountWhen, PropsError};
74pub use plugin::{PLUGIN_NAME, ReactPlugin};
75pub use tags::{react_bundle, react_script};
76pub use update::{PROPS_EVENT, PropsUpdate};