React integration
Mount an ExoJS Application inside a React tree and drive scenes declaratively with the @codexo/exojs-react bindings.
React integration
The official @codexo/exojs-react package lets you host an ExoJS
Application inside a React component tree: it owns the
canvas, manages the app lifecycle for you, switches Scenes
declaratively, and lets React HUD overlays read the running app through context.
It is a plain React binding, not an engine extension — there is no /register entry and
nothing to wire into ApplicationOptions. You use it from .tsx files alongside the rest of
your React UI.
Note: This package sits next to the core engine and needs React as a peer. Install all three:
npm install @codexo/exojs @codexo/exojs-react react
@codexo/exojsandreact(>= 18) are peer dependencies;react-domis the usual host renderer. The package ships pre-built ESM with type declarations and type-checks against both@types/react18 and 19.
Two layers
The package is intentionally split into two layers — pick the one that matches how much DOM control you want:
useExoApplication— headless. A hook that creates and owns theApplicationand binds it to a<canvas>you render. It produces no DOM of its own, so you keep full control over the canvas element, its container, and its styling.<ExoCanvas>— batteries-included. A component that renders a positioned wrapper<div>plus a React-managed<canvas>, and provides the app to descendants via context. HUD overlays and the declarative scene API work out of the box.
<ExoCanvas> is built on top of useExoApplication; everything below the scene API applies to
both.
The headless hook
useExoApplication(options?, onReady?) returns the app and a ref to attach to your own canvas:
import { useExoApplication } from '@codexo/exojs-react';
function Game() {
const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });
// `app` is null until the canvas is mounted; render it however you like.
return <canvas ref={canvasRef} className="game-surface" />;
}
The hook returns { app, canvasRef }: app is the Application (or null until the canvas
mounts and the app is created), and canvasRef is a stable ref you attach to the <canvas> the
app should bind to. The optional onReady callback fires once each time an app is created.
Lifecycle and reactivity
options is an ExoApplicationOptions — the same shape as ApplicationOptions, but the
canvas.element / canvas.mount fields are managed for you (the hook binds the app to the
canvas it references). You still pass canvas.width, canvas.height, canvas.sizingMode,
clearColor, backend, and so on.
The hook treats most options as captured-at-creation, but live-syncs a few without tearing the app down:
- The app is recreated only when the render
backendchanges — WebGL2 ↔ WebGPU cannot be hot-swapped, so this is the one identity option. canvas.width/canvas.heightare applied live viaapp.resize(...).canvas.sizingModeis applied live via theapp.sizingModesetter.clearColoris applied live via theapp.clearColorsetter (keyed on its channel values, so a freshColorwith identical channels does not re-assign).- Options without a live setter (
canvas.pixelRatio,seed,extensions, …) are captured at creation. Change thebackendor remount to apply them.
On unmount the hook calls app.destroy(). The engine never removes a canvas it did not create,
so React stays the sole owner of the <canvas> element.
Note: With the default
'fixed'sizing mode the engine never touches the canvas CSS, so you may style it freely. The'fill','fit','shrink', and'letterbox'modes managecanvas.stylethemselves — size the container and let them observe it, rather than fighting them with an inlinestyle.
The batteries-included canvas
<ExoCanvas> renders a position: relative wrapper <div> containing the canvas, and provides
the app via context so descendants (HUD, the scene API) can reach it. Because the wrapper is
positioned, absolutely-positioned children sit over the canvas with no extra setup.
import { ExoCanvas } from '@codexo/exojs-react';
function Game() {
return (
<ExoCanvas
options={{ canvas: { width: 1280, height: 720 } }}
style={{ width: 1280, height: 720 }}
>
<div style={{ position: 'absolute', top: 8, left: 8 }}>HUD overlay</div>
</ExoCanvas>
);
}
Props:
options— theExoApplicationOptionsforwarded to the app (same reactivity as above).onReady— called once each time the app is (re)created.- Layout props (
style,className, and any other<div>attributes) apply to the wrapper. Size the wrapper to drive the'fill'/'letterbox'sizing modes. canvasProps— forwarded to the inner<canvas>(its ownstyle/className);ref,width, andheightare managed by the engine and cannot be set here.children— rendered as an overlay once the app exists, with the app available via context.
For full control with no wrapper element, drop down to useExoApplication.
Declarative scenes
<Scenes> switches the one active scene by name. Declare each scene with <Scene> and select
the active one through the active prop. The first activation calls app.start(scene) (which
initializes the backend and starts the frame loop); later switches call
app.scene.setScene(scene, …) with the optional transition.
The scene classes you reference are ordinary ExoJS scenes — the React layer only decides which one is active:
import { Scene } from '@codexo/exojs';
export class TitleScene extends Scene {}
export class GameScene extends Scene {}
import { ExoCanvas, Scenes, Scene } from '@codexo/exojs-react';
import { TitleScene, GameScene } from './scenes';
function Game({ screen }: { screen: 'title' | 'game' }) {
return (
<ExoCanvas options={{ canvas: { width: 1280, height: 720 } }} style={{ width: 1280, height: 720 }}>
<Scenes active={screen} transition={{ type: 'fade', duration: 300 }}>
<Scene name="title" component={TitleScene} />
<Scene name="game" component={GameScene}>
<Hud />
</Scene>
</Scenes>
</ExoCanvas>
);
}
Each <Scene> takes a unique name, the scene component class to instantiate, and optional
children that render as the active scene’s overlay. The transition (a core SceneTransition,
e.g. a fade whose duration is in milliseconds) only applies to switches, not the first start.
If active matches no <Scene>, the active scene is cleared.
useActiveScene() reads the live scene instance from the nearest <Scenes>, so an overlay can
react to scene state:
import { useActiveScene } from '@codexo/exojs-react';
import type { GameScene } from './scenes';
function Hud() {
const scene = useActiveScene<GameScene>();
if (scene === null) return null;
return <div style={{ position: 'absolute', top: 8, left: 8 }}>Score: {scene.score}</div>;
}
For the simplest case — a single scene with no switching — use useScene(SceneClass, deps?)
instead. It instantiates and activates one scene, returning the instance once it is live (or
null while loading), and clears it on unmount or when deps change.
Reaching the app from descendants
Any component rendered inside <ExoCanvas> can read the running app:
useExoApp()returns theApplicationand throws an actionable error if there is no<ExoCanvas>ancestor — use it in components that require the app.useExoContext()returnsApplication | null(no throw) for optional access.ExoContextis the underlying context object, exported for advanced use (testing, custom providers).
app.frameCount is a plain getter the engine updates on its own frame loop — reading it
alone does not make a component re-render. Pair useExoApp() with useSignal() to subscribe
to app.onFrame and re-render on every dispatch:
import { useExoApp, useSignal } from '@codexo/exojs-react';
function FrameCounter() {
const app = useExoApp(); // throws if rendered outside <ExoCanvas>
const frameCount = useSignal(app.onFrame, () => app.frameCount);
return <span>Frame: {frameCount}</span>;
}
End-to-end
A small app that hosts the canvas, switches between two scenes with a fade, and overlays React HUD on the active scene:
import { useState } from 'react';
import { ExoCanvas, Scenes, Scene, useActiveScene } from '@codexo/exojs-react';
import { TitleScene, GameScene } from './scenes';
function Hud() {
const scene = useActiveScene();
return (
<div style={{ position: 'absolute', top: 8, left: 8, color: 'white' }}>
{scene?.constructor.name}
</div>
);
}
export function App() {
const [screen, setScreen] = useState<'title' | 'game'>('title');
return (
<ExoCanvas
options={{ canvas: { width: 1280, height: 720 } }}
style={{ width: 1280, height: 720 }}
>
<Scenes active={screen} transition={{ type: 'fade', duration: 300 }}>
<Scene name="title" component={TitleScene}>
<button style={{ position: 'absolute', inset: 0 }} onClick={() => setScreen('game')}>
Start
</button>
</Scene>
<Scene name="game" component={GameScene}>
<Hud />
</Scene>
</Scenes>
</ExoCanvas>
);
}
React owns the screen state and the HUD; ExoJS owns the canvas, the renderer, and the per-frame loop. The two stay cleanly separated.
Where to look next
- Package README:
@codexo/exojs-react— install, the export table, and the reactivity model. - Scenes & lifecycle: the Scenes & lifecycle
chapter explains
app.start,app.scene.setScene, transitions, and the hooks the React layer drives for you. - API reference: the
ApplicationandScenepages document the underlying engine surface the bindings wrap.