Skip to content

Phaser Hooks vs MobX

Both Phaser Hooks and MobX are great tools for managing state, but they serve different worlds.
While MobX was built for UI frameworks like React or Vue, Phaser Hooks was designed specifically for Phaser’s game loop, giving you predictable, scene-aware reactivity with almost zero overhead.

This page provides an honest, developer-oriented comparison so you can choose what best fits your project.

Aspectphaser-hooksMobX
Learning CurveLow - Simple APIMedium - Concepts like observables, autorun, reactions
Bundle Size~10KB~63KB
TypeScriptFirst-class supportFirst-class support
BoilerplateMinimalMinimal
ReactivityControlled and explicit (ideal for game loops)Automatic tracking in DOM, but in Phaser you must listen autorun manually
Phaser IntegrationNative (registry + scene lifecycle)External, requires manual cleanup
PerformancePredictable, targeted updatesOptimized, but generalized
EcosystemPurpose-built for PhaserGeneral-purpose (React, Vue, etc.)

hooks/withPlayer.ts
import { type HookState, withLocalState } from 'phaser-hooks';
type Player = {
hp: number;
maxHp: number;
level: number;
};
type PlayerState = HookState<Player> & {
takeDamage: (amount: number) => void;
heal: (amount: number) => void;
isDead: () => boolean;
};
const initialState: Player = {
hp: 100,
maxHp: 100,
level: 1,
};
export function withPlayer(scene: Phaser.Scene): PlayerState {
const state = withLocalState<Player>(scene, 'player', initialState);
return {
...state,
takeDamage: (amount: number) => {
const current = state.get();
state.patch({ hp: Math.max(0, current.hp - amount) });
},
heal: (amount: number) => {
const current = state.get();
state.patch({ hp: Math.min(current.maxHp, current.hp + amount) });
},
isDead: () => state.get().hp <= 0,
};
}
// Usage in scene
import { withPlayer } from './hooks/withPlayer';
class GameScene extends Phaser.Scene {
private unsubscribe?: () => void;
create() {
const player = withPlayer(this);
// Update state
player.takeDamage(30);
// Listen to changes (manual)
this.unsubscribe = player.on('change', (newPlayer) => {
console.log('HP:', newPlayer.hp);
this.updateHealthBar(newPlayer.hp, newPlayer.maxHp);
if (player.isDead()) {
this.scene.start('GameOverScene');
}
});
}
shutdown() {
this.unsubscribe?.();
}
}

const player = withPlayer(this);
player.on('change', (newValue, oldValue) => {
if (newValue.hp !== oldValue.hp) {
this.updateHealthBar(newValue.hp);
}
});

Pros:

  • Clear and predictable - you know exactly when callbacks fire, no hidden side effects
  • Fine-grained control over what triggers updates
  • Perfect for game loops where over-reactivity hurts FPS
  • Automatic cleanup with scene shutdown in local state
  • Easier debugging (no “why did this re-run?” surprises)
  • No magic - straightforward mental model
  • No need unsubscribe in local state

⚠️ Considerations:

  • Need to call .get() to read state
  • You cannot share the state with DOM elements in page, only in Phaser
autorun(() => {
this.updateHealthBar(playerStore.hp);
});

Pros:

  • Automatic dependency tracking - no manual subscriptions
  • Built-in computed properties
  • Great for complex UI scenarios

⚠️ Considerations:

  • Not designed for game loops - automatic tracking can be overkill
  • Can be harder to debug (why did this re-run?)
  • Potential for accidental over-reactivity
  • Requires manual cleanup in Phaser scene shutdown
  • Multiple different ways to implement the same thing (observers, decorators, classes, etc.)

Phaser Hooks: Minimal and Game-Focused

state.get() // Read
state.set(value) // Write (full)
state.patch(partial) // Write (partial)
state.on('change', callback) // Subscribe

Pros:

  • Clean, predictable, and small
  • Integrates directly with Phaser’s registry
  • No decorators or special syntax
  • Works seamlessly with Phaser’s existing patterns
  • Functional programming friendly

⚠️ Considerations:

  • Not meant for web UIs — 100% focused on game state

MobX: Feature-Rich for Apps

makeAutoObservable(this) // Make object reactive
autorun(() => {}); // Automatically tracks dependencies
reaction(() => {});
computed get healthPercent() {};
runInAction // Batch updates

Pros:

  • Mature ecosytem and advanced patterns
  • Powerful reaction APIs for complex scenarios
  • Shared state between Phaser and React/Vue/Vanilla JS.

⚠️ Considerations:

  • Larger API surface - more to learn
  • Decorators can be confusing for beginners
  • Requires understanding of observables/observers
  • More complexity and concepts to learn
  • Requires manual cleanup in Phaser scene shutdown

phaser-hooks: Native Integration

// Uses Phaser's built-in registry under the hood
const player = withLocalState<Player>(this, 'player', { hp: 100 });

Pros:

  • Built from the ground up for Phaser
  • Leverages Phaser’s registry system
  • Automatic cleanup with scene lifecycle in local state
  • No extra dependencies
  • Smaller bundle size

⚠️ Considerations:

  • Focused solely on Phaser (not intended for non-game apps)

MobX: Generic Integration

// Bring your own store management
import { playerStore } from './stores/PlayerStore';
// Must manually integrate with Phaser lifecycle
this.events.once('shutdown', () => disposer());

Pros:

  • Framework-agnostic - use in React, Vue, vanilla JS, etc.
  • Can be shared across app layers (e.g. React + Phaser)

⚠️ Considerations:

  • Additional dependency (~63KB)
  • Requires manual cleanup
  • Doesn’t understand Phaser’s scene lifecycle

Both are fast, but their optimizations target different needs.

  • Phaser Hooks: designed for deterministic updates, you control what and when to update. Perfect for maintaining steady FPS.
  • ** MobX:** optimized for UI dependency graphs — excellent for data-driven interfaces, but overkill for frame-by-frame logic.

🏆 Best fit for games: Phaser Hooks 🏆 Best fit for UIs: MobX

Section titled “🏆 Best fit for games: Phaser Hooks 🏆 Best fit for UIs: MobX”

phaser-hooks:

const player = withPlayer(this);
const healthPercent = withComputedState(
this,
'healthPercent',
player,
(p) => (p.hp / p.maxHp) * 100
);
healthPercent.get(); // 100

Pros:

  • Explicit and clear logic
  • Full control over recalculation

⚠️ Considerations:

  • More boilerplate

MobX:

class PlayerStore {
hp = 100;
maxHp = 100;
get healthPercent() {
return (this.hp / this.maxHp) * 100;
}
}

Pros:

  • Auto memoization
  • Elegant syntax

⚠️ Considerations:

  • Implicit dependency tracking can confuse beginners

Both libraries offer strong typing and inference, ensuring safe and predictable updates.

phaser-hooks:

type Player = { hp: number; level: number };
const player = withLocalState<Player>(this, 'player', { hp: 100, level: 1 });
player.get().hp; // TypeScript knows: number
player.patch({ hp: 50 }); // ✅ Type-safe
player.patch({ invalid: true }); // ❌ Type error

MobX:

class PlayerStore {
hp: number = 100;
level: number = 1;
takeDamage(amount: number) {
this.hp -= amount;
}
}
playerStore.hp; // TypeScript knows: number
playerStore.takeDamage(10); // ✅ Type-safe
playerStore.takeDamage('invalid'); // ❌ Type error

phaser-hooks: ~10KB gzipped (minimal) MobX: ~63KB gzipped (core only)

If you care about initial load times or embedding games in iframes, Phaser Hooks wins easily.

Section titled “If you care about initial load times or embedding games in iframes, Phaser Hooks wins easily.”

phaser-hooks:

// Easy to learn - 4 core concepts:
state.get() // Read
state.set() // Write
state.on() // Subscribe
unsubscribe() // Clean up

Time to productivity: ~30 minutes


MobX:

// More concepts to learn:
makeAutoObservable() // Observables
@computed // Computed values
@action // Actions
autorun() // Reactions
reaction() // Custom reactions
runInAction() // Transactions

Learn observables, actions, computed, autorun, reactions, decorators… Time to productivity: ~2-3 hours


Choose phaser-hooks if:

  • You’re building a Phaser-only game
  • You prefer explicit, predictable updates over automatic reactivity
  • You value small bundles and fast loading
  • You want quick onboarding for your team
  • You need tight Phaser integration (automatic cleanup, etc.)
  • You like functional programming patterns (hooks, not classes)

Phaser Hooks is not trying to be Mobx, it’s a precision tool built specifically for game developers.

Section titled “Phaser Hooks is not trying to be Mobx, it’s a precision tool built specifically for game developers.”

Choose MobX if:

  • You need automatic reactivity for complex UIs
  • You’re already using MobX in other parts of your app
  • You want to share state between Phaser and React/Vue
  • You’re building a large, complex game with deep reactive dependencies

Yes! They’re not mutually exclusive, you can use both in the same project:

// Use phaser-hooks for Phaser-specific state
const player = withPlayer(this);
// Use MobX for complex UI state
import { uiStore } from './stores/UIStore';
autorun(() => {
// React to MobX store
if (uiStore.showInventory) {
this.showInventoryUI();
}
});

// Before (MobX)
class PlayerStore {
hp = 100;
takeDamage(amount: number) { this.hp -= amount; }
}
// After (phaser-hooks)
export function withPlayer(scene: Phaser.Scene) {
const state = withLocalState(scene, 'player', { hp: 100 });
return {
...state,
takeDamage: (amount: number) => {
state.patch({ hp: state.get().hp - amount });
}
};
}

CriteriaWinner
Simplicity✅ Phaser Hooks
Bundle Size✅ Phaser Hooks
Phaser Integration✅ Phaser Hooks
Scene Lifecycle Integration✅ Phaser Hooks
Computed Values✅ MobX
Automatic Reactivity✅ MobX
Framework Agnostic✅ Mobx
Learning Curve✅ Phaser Hooks
Mature Ecosystem✅ MobX

MobX is a fantastic general-purpose reactivity engine. But Phaser Hooks is crafted for one purpose — to make state management inside Phaser games simple, stable, and fast.

If your focus is on performance, clarity, and game-specific integration, Phaser Hooks gives you the precision you need — without the overhead of a UI framework.