Skip to content

UI (React)

The @yagejs/ui-react package provides a React reconciler over the UI system, letting you write game UI with JSX. It includes hooks for accessing engine services and reactive stores for bridging ECS state into React.

Install both @yagejs/ui and @yagejs/ui-react, plus React:

Terminal window
npm install @yagejs/ui @yagejs/ui-react react

The UIPlugin must be registered before using React UI:

import { UIPlugin } from "@yagejs/ui";
engine.use(new UIPlugin());

UIRoot is a component that hosts a React tree on an entity:

import { UIRoot } from "@yagejs/ui-react";
import { Anchor } from "@yagejs/ui";
const ui = scene.spawn("ui");
ui.add(new Transform());
const root = new UIRoot({ anchor: Anchor.Center });
ui.add(root);
root.render(<MyMenu />);

The React tree renders into the screen-space UI layer, positioned by the anchor and optional offset.

React components mirror the imperative @yagejs/ui API:

import { Panel, Text, Button, Image, ProgressBar, Checkbox } from "@yagejs/ui-react";
function HUD() {
const [score, setScore] = useState(0);
return (
<Panel direction="column" gap={8} padding={16} bg={{ color: 0x000000, alpha: 0.7 }}>
<Text style={{ fontSize: 24, fill: 0xffffff }}>Score: {score}</Text>
<Button
width={150}
height={40}
bg={{ color: 0x4444aa }}
hoverBg={{ color: 0x5555cc }}
textStyle={{ fontSize: 16, fill: 0xffffff }}
onClick={() => setScore(s => s + 1)}
>
Add Point
</Button>
<ProgressBar
width={200}
height={16}
value={score / 10}
fillBackground={{ color: 0x44cc44 }}
trackBackground={{ color: 0x333333 }}
/>
<Checkbox
label="Mute"
checked={false}
onChange={(v) => console.log("mute:", v)}
/>
</Panel>
);
}
ComponentDescription
<Panel>Flexbox container with direction, gap, padding, bg
<Text>Text label with style
<Button>Clickable button with onClick, bg, hoverBg, pressBg
<Image>Texture display with texture, tint, alpha
<NineSlice>Nine-slice scalable texture
<ProgressBar>Progress bar (value 0–1)
<Checkbox>Checkbox with checked, onChange, label

Wrappers for advanced @pixi/ui widgets:

import {
PixiFancyButton,
PixiSlider,
PixiInput,
PixiSelect,
PixiRadioGroup,
PixiScrollBox,
} from "@yagejs/ui-react";

Access the engine context or current scene from any React component:

import { useEngine, useScene } from "@yagejs/ui-react";
function PauseButton() {
const engine = useEngine();
return (
<Button onClick={() => engine.scenes.push(new PauseScene())} width={100} height={40}>
Pause
</Button>
);
}

Bridge ECS state into React with reactive stores:

import { createStore } from "@yagejs/ui-react";
import { useStore } from "@yagejs/ui-react";
// Create a store (typically at scene level)
const gameStore = createStore({ score: 0, health: 100 });
// Write from ECS (systems, components, event handlers)
gameStore.set({ score: gameStore.get().score + 10 });
// Read from React (auto-rerenders on change)
function ScoreDisplay() {
const score = useStore(gameStore, (s) => s.score);
return <Text style={{ fontSize: 32 }}>{`Score: ${score}`}</Text>;
}

The optional selector and equality function prevent unnecessary re-renders:

// Only re-render when score changes
const score = useStore(store, (s) => s.score);
// Custom equality check
const pos = useStore(store, (s) => s.position, (a, b) => a.x === b.x && a.y === b.y);

Query ECS entities directly from React:

import { useQuery } from "@yagejs/ui-react";
function EnemyCount() {
const count = useQuery(
[EnemyTag],
(result) => result.size,
);
return <Text>Enemies: {count}</Text>;
}

The query re-evaluates each frame and only triggers a re-render when the selector result changes.

Read arbitrary scene state:

import { useSceneSelector } from "@yagejs/ui-react";
function EntityCounter() {
const count = useSceneSelector((scene) => scene.getEntities().length);
return <Text>Entities: {count}</Text>;
}
Imperative (@yagejs/ui)React (@yagejs/ui-react)
Best forSimple HUDs, static menusComplex interactive menus, forms
StateManual .setText() callsDeclarative with useState / useStore
LayoutBuilder APIJSX composition
Bundle sizeSmaller (no React)Adds React dependency
Learning curveLower if no React experienceLower if familiar with React

Use the imperative API for simple, mostly-static UI (score counters, health bars). Use React when your UI has complex state, conditional rendering, or many interactive elements.