Skip to main content

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}