State Management
YAGE provides several ways to manage game state depending on your needs: DI-registered services for shared state, reactive stores for UI binding, and events for decoupled communication.
Service Key Pattern
Section titled “Service Key Pattern”The most common approach is a plain class registered as a DI service:
import { ServiceKey } from "@yagejs/core";
interface GameState { score: number; health: number; level: number;}
const GameStateKey = new ServiceKey<GameState>("gameState");Register it in your scene:
class GameScene extends Scene { onEnter(): void { const state: GameState = { score: 0, health: 100, level: 1 }; this.context.register(GameStateKey, state); }}Access it from any component or system in the scene:
class ScoreDisplay extends Component { private readonly state = this.use(GameStateKey);
update(): void { this.state.score; // read }}This pattern is simple, type-safe, and works well with the save system — just
add serialize()/fromSnapshot() methods if the state needs to persist.
Reactive Stores (for React UI)
Section titled “Reactive Stores (for React UI)”When bridging ECS state to React UI, use createStore:
import { createStore } from "@yagejs/ui-react";
const uiStore = createStore({ score: 0, health: 100, message: "",});Write from ECS (systems, components, event handlers):
// In a component or event handleruiStore.set({ score: uiStore.get().score + 10 });Read from React (auto-rerenders on change):
import { useStore } from "@yagejs/ui-react";
function HUD() { const score = useStore(uiStore, (s) => s.score); return <Text>{`Score: ${score}`}</Text>;}The store acts as a one-way bridge: ECS writes, React reads. This keeps game logic in the ECS layer and rendering in the UI layer.
Event-Driven State
Section titled “Event-Driven State”For decoupled state changes, use typed events:
import { defineEvent } from "@yagejs/core";
const CoinCollected = defineEvent("coin:collected");const PlayerDied = defineEvent("player:died");Emit from entities (via entity events — bubble up to the scene):
// In a coin's trigger handlercollider.onTrigger((ev) => { if (ev.entered) { this.entity.emit(CoinCollected); this.entity.destroy(); }});Listen at the scene level:
class GameScene extends Scene { onEnter(): void { this.on(CoinCollected, () => { state.score += 10; uiStore.set({ score: state.score }); });
this.on(PlayerDied, () => { state.health = 100; state.score = 0; // Reset player position... }); }}This keeps entities decoupled — a coin doesn’t need to know about the score system, it just emits an event.
Combining Patterns
Section titled “Combining Patterns”A typical game combines all three:
// 1. Game state as a DI serviceconst GameStateKey = new ServiceKey<GameState>("gameState");
// 2. UI store for React bindingconst uiStore = createStore({ score: 0, health: 100 });
// 3. Events for decoupled communicationconst CoinCollected = defineEvent("coin:collected");
class GameScene extends Scene { onEnter(): void { const state: GameState = { score: 0, health: 100, level: 1 }; this.context.register(GameStateKey, state);
this.on(CoinCollected, () => { state.score += 10; uiStore.set({ score: state.score }); }); }}- ServiceKey — source of truth, accessible from any component
- Store — React-friendly projection of the state
- Events — triggers that cause state transitions
Serialization
Section titled “Serialization”If your state service needs to survive save/load, make it serializable:
class GameStateService { score = 0; health = 100; level = 1;
serialize() { return { score: this.score, health: this.health, level: this.level }; }
hydrate(data: { score: number; health: number; level: number }) { this.score = data.score; this.health = data.health; this.level = data.level; }}Call hydrate() in the scene’s afterRestore() hook with the saved data.
Anti-Patterns
Section titled “Anti-Patterns”- Global mutable state — avoid
window.scoreor module-levellet score. This breaks scene isolation and makes testing difficult. - Component-to-component direct references — don’t reach into another entity’s components to modify their state. Use events or shared services.
- Storing state in the UI layer — keep game logic state in the ECS layer. The UI should read state, not own it.