bevy-react is a custom React renderer for Bevy UI. It embeds the Boa JavaScript engine in a Bevy plugin, runs React inside that engine, and translates reconciler operations into Bevy ECS mutations.
This document summarizes the design described in CLAUDE.md and the gaps tracked in PROJECT_PLAN.md.
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ JS / React (Boa) │ │ Rust / Bevy │
│ │ RPC │ │
│ createBevyApp / render │ ──────► │ ReactClientProto messages │
│ react-reconciler host │ │ process_react_messages │
│ __react_* native fns │ ◄────── │ Interaction / keyboard │
│ │ events │ → synthetic JS events │
└─────────────────────────────┘ └──────────────────────────────┘
- Host (Rust):
ReactPluginplusjs_bevy::JsPlugin. On native targets the JS engine runs on a dedicated OS thread; onwasm32it uses a WASM-compatible async path. - Client (TS):
packages/bevy-reactimplements areact-reconcilerhost config. Mutations call globals such as__react_create_node,__react_append_child, etc., registered by the Rust host (native_functions.rs).
Apps default-export createBevyApp(<App />). The host loads the ESM module and calls mod.default.render(rootId).
Messages flow JS → Rust via ReactClientProto (plugin/src/react/client.rs):
| Message | Purpose |
|---|---|
CreateNode / CreateText |
Allocate a node id and spawn an entity |
AppendChild / InsertBefore / RemoveChild |
Tree structure |
UpdateNode / UpdateText |
Props / text content |
DestroyNode / ClearContainer |
Teardown |
Bevy systems in plugin/src/react/systems/render.rs apply these to entities tagged with ReactNode / ReactTextNode, under a root created by ReactBundle.
React style objects are JSON-serialized with node props. Rust deserializes StyleProps (style.rs), builds a Bevy Node via json_to_style, and applies visual components (BackgroundColor, BorderColor, BorderRadius, ZIndex, text color/font) in the render system.
Details and prop tables: STYLE_PROPS.md.
plugin/src/react/systems/input.rs observes Bevy Interaction (and hover) plus keyboard state for focused nodes, then invokes into the JS engine to fire handlers (onClick, hover, key down, etc.). Event delivery is still relatively coarse (see Epic 3 in the project plan — eval-based dispatch, missing press/release, limited focus).
ViteDevSource: Bootstraps Vite's client and the app entry from a dev server. Useswebsocket/fetchCargo features on native.ReactScriptSource::from_path/from_string: Load a prebuilt or inline module without HMR.- Production asset-pipeline loading (via Bevy
AssetServer) is planned (Epic 6), not finished.
Conditional compilation swaps clocks, threading, and tokio features for wasm32. Job flushing is currently a no-op on WASM, so promises/timers and async event paths are limited — tracked under Epic 5.
| Area | Path |
|---|---|
| Bevy plugin entry | plugin/src/lib.rs |
| JS engine (native) | plugin/src/js/engine.rs |
| JS engine (WASM) | plugin/src/js/engine_wasm.rs |
| Native JS functions | plugin/src/react/native_functions.rs |
| Data bridge | plugin/src/react/bridge.rs, packages/bevy-react/src/bridge.ts |
| RPC client | plugin/src/react/client.rs |
| Style conversion | plugin/src/react/style.rs |
| Render system | plugin/src/react/systems/render.rs |
| Input system | plugin/src/react/systems/input.rs |
| Vite / HMR bootstrap | plugin/src/react/vite.rs |
| TS reconciler | packages/bevy-react/src/reconciler.ts |
| TS components | packages/bevy-react/src/components/ |
| Demo | examples/demo/ |
Foundation (render, styles, events, multi-root, examples) is in place; remaining gaps are roadmap-driven — details in PROJECT_PLAN.md:
- Game-state bridge is minimal/
ReactBridgechannels, not typed stores + codegen (BRIDGE.md). - Interaction styling and transitions are still JS-side (
useInteraction), not host-applied from BevyInteraction. - Widgets are compositional TS; host
bevy_ui_widgetsmapping is incomplete. - No public ECS escape hatch (
ref→Entity, named bundle attach). - Runtime errors log via
JsRuntimeError; no in-game overlay / style diagnostics yet. - Packages are unpublished; treat as path-dependency only.