deckyfx_dioxus_react_integration/container.rs
1//! React Container Component
2//!
3//! Provides a Dioxus component for mounting React applications using a
4//! folder asset + manifest pattern. The build step generates `metadata.json`
5//! describing the output files, and `ReactApp` reads it to load the correct
6//! CSS/JS chunks — no manual renaming or copying needed.
7//!
8//! # Breaking Changes (0.2.0)
9//!
10//! - Removed `ReactContainer` (single bundle.js only)
11//! - `ReactApp` now uses `dir` (folder asset) + `manifest` (ReactManifest) instead
12//! of individual `bundle` / `stylesheet` asset props
13//!
14//! # Usage
15//!
16//! ```rust,ignore
17//! const REACT_DIR: Asset = asset!("/assets/react");
18//!
19//! // Parse metadata.json at compile time
20//! let manifest = ReactManifest::from_json(
21//! include_str!("../assets/react/metadata.json")
22//! ).expect("invalid metadata.json");
23//!
24//! rsx! {
25//! ReactApp {
26//! dir: REACT_DIR,
27//! manifest: manifest,
28//! bridge: bridge,
29//! }
30//! }
31//! ```
32
33use dioxus::prelude::*;
34use serde::Deserialize;
35
36/// Build manifest describing the React app's output files.
37///
38/// Generated by the build step as `metadata.json` in the dist folder.
39/// Contains the hashed filenames so the Rust side can load them correctly.
40///
41/// # Example metadata.json
42/// ```json
43/// {
44/// "scripts": ["chunk-2b2s0nqa.js"],
45/// "styles": ["chunk-q7kzc8rh.css"],
46/// "assets": ["logo-kygw735p.svg", "react-c5c0zhye.svg"]
47/// }
48/// ```
49#[derive(Debug, Clone, PartialEq, Deserialize)]
50pub struct ReactManifest {
51 /// JS bundle filenames (order matters — loaded in sequence)
52 pub scripts: Vec<String>,
53 /// CSS stylesheet filenames
54 #[serde(default)]
55 pub styles: Vec<String>,
56 /// Other asset filenames (images, fonts, etc.) — informational only
57 #[serde(default)]
58 pub assets: Vec<String>,
59}
60
61impl ReactManifest {
62 /// Parse a ReactManifest from a JSON string.
63 ///
64 /// Typically used with `include_str!` to embed metadata.json at compile time:
65 /// ```rust,ignore
66 /// let manifest = ReactManifest::from_json(
67 /// include_str!("../assets/react/metadata.json")
68 /// ).expect("invalid metadata.json");
69 /// ```
70 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
71 serde_json::from_str(json)
72 }
73}
74
75/// React application container — loads a React app from a folder asset + manifest.
76///
77/// Uses Dioxus's folder asset feature to reference the entire React build output
78/// directory. The manifest describes which CSS/JS files to load (with their
79/// hashed filenames), so no renaming or copying is needed.
80///
81/// The component:
82/// 1. Loads all stylesheets listed in the manifest
83/// 2. Injects the IPC bridge script (if provided)
84/// 3. Creates the mount point `<div>` and loads all JS bundles
85///
86/// # Example
87/// ```rust,ignore
88/// use dioxus::prelude::*;
89/// use dioxus_react_integration::prelude::*;
90/// use std::time::Duration;
91///
92/// const REACT_DIR: Asset = asset!("/assets/react");
93///
94/// fn app() -> Element {
95/// let bridge = IpcBridge::builder()
96/// .timeout(Duration::from_secs(30))
97/// .build();
98///
99/// let manifest = ReactManifest::from_json(
100/// include_str!("../assets/react/metadata.json")
101/// ).expect("invalid metadata.json");
102///
103/// rsx! {
104/// ReactApp {
105/// dir: REACT_DIR,
106/// manifest: manifest,
107/// bridge: bridge,
108/// }
109/// }
110/// }
111/// ```
112#[component]
113pub fn ReactApp(
114 /// Folder asset pointing to the React build output directory
115 dir: Asset,
116 /// Build manifest with the hashed filenames
117 manifest: ReactManifest,
118 /// Optional IPC bridge instance (generates and injects the bridge script)
119 bridge: Option<crate::prelude::IpcBridge>,
120 /// DOM element ID where React will mount (default: "root")
121 #[props(default = "root".to_string())]
122 mount_id: String,
123) -> Element {
124 let bridge_script = bridge.map(|b| b.generate_script());
125
126 // The asset folder path (e.g. "/assets/react/") — used as <base> so that
127 // relative asset references in the JS bundle (like "./logo.svg") resolve
128 // correctly against the Dioxus asset directory.
129 let base_href = format!("{}/", dir);
130
131 // Build full paths: "{dir}/{filename}"
132 let style_paths: Vec<String> = manifest
133 .styles
134 .iter()
135 .map(|s| format!("{}/{}", dir, s))
136 .collect();
137
138 let script_paths: Vec<String> = manifest
139 .scripts
140 .iter()
141 .map(|s| format!("{}/{}", dir, s))
142 .collect();
143
144 rsx! {
145 // 1. Set <base> so relative URLs in the React bundle resolve to the asset folder.
146 // Dioxus desktop serves assets at /assets/react/ but the document base is "/".
147 // Without this, "./logo.svg" in JS resolves to "/logo.svg" (404) instead of
148 // "/assets/react/logo.svg".
149 script {
150 dangerous_inner_html: "document.head.insertBefore(Object.assign(document.createElement('base'), {{ href: '{base_href}' }}), document.head.firstChild);"
151 }
152
153 // 2. Load all stylesheets
154 for css_path in &style_paths {
155 link { rel: "stylesheet", href: "{css_path}" }
156 }
157
158 // 3. Inject IPC bridge script before React loads
159 if let Some(ref script_content) = bridge_script {
160 script { dangerous_inner_html: "{script_content}" }
161 }
162
163 // 4. React mount point
164 div { id: "{mount_id}" }
165
166 // 5. Load all JS bundles
167 for js_path in &script_paths {
168 script { src: "{js_path}" }
169 }
170 }
171}