phaser-hooks
v0.7.2
Published
Hooks in react-style for Phaser games
Maintainers
Readme
Phaser Hooks
React-like state management for Phaser 3 games. Simple, type-safe, and powerful.
Why phaser-hooks?
Phaser gives you registry (global) and data (local) for state management. They work fine, but the API is verbose and error-prone:
// Phaser's built-in way - this == scene
this.game.registry.set('volume', 0.5);
const volume = this.game.registry.get('volume');
this.game.registry.events.on('changedata-volume', (game, value) => {
console.log('Volume changed to', value);
});
this.data.set('score', 42);
const score = this.data.get('score');
this.onChangeFn = (scene, value) => {
console.log('Score updated to', value);
};
this.data.events.on('changedata-score', this.onChangeFn); // if you pass an anonymous function, you cannot unsubscribe
// when move to another scene, you must unsubscribe. Boring and easy to forget
this.data.events.off('changedata-score', this.onChangeFn);With phaser-hooks, you get a simple, React-like API:
const volume = withGlobalState(this, 'volume', 0.5);
volume.get(); // Returns: 0.5
volume.set(0.8); // updates the value
this.unsubscribe = volume.on('change', () => {
console.log('Volume changed →', volume.get())
}); // Returns the easy unsubscribe function
// when changing scenes
this.unsubscribe();Key Benefits
- ✅ React-like patterns - Hooks work just like React: same key = same state
- ✅ Type-safe - Full TypeScript support with inference
- ✅ Memory safe - Auto-cleanup prevents memory leaks
- ✅ Feature-rich - Persistence, computed state, undo/redo, validation
- ✅ Familiar - React-like patterns for easier onboarding
Installation
npm install phaser-hooks
# or
pnpm add phaser-hooks
# or
yarn add phaser-hooksNote: This library uses "with" prefix (e.g.,
withLocalState) instead of "use" to avoid ESLint warnings in.tsfiles.
🌐 UMD/CDN (JavaScript)
If you prefer not to use TypeScript or want to include the library via CDN, you can use the UMD build:
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser-hooks.min.js"></script>The library will be available globally as window.PhaserHooks. You can use it like this:
// Create local state
const playerState = window.PhaserHooks.withLocalState(scene, 'player', { hp: 100 });⚠️ Note: While UMD builds are available, we strongly recommend using TypeScript for better type safety, IntelliSense, and development experience. The TypeScript version provides better error detection and autocomplete features.
Quick Start
Here's a complete example showing the basics:
// hooks/withPlayerState.ts
import { withLocalState } from 'phaser-hooks';
// or const { withLocalState } from 'phaser-hooks';
const withPlayer = (scene: Phaser.Scene) => {
const player = withLocalState(scene, 'player', {
hp: 100,
maxHp: 100,
level: 1,
});
return player;
};
// hooks/withSettings.ts
import { withGlobalState } from 'phaser-hooks';
const withSettings = (scene: Phaser.Scene) => {
const settings = withGlobalState(scene, 'settings', {
volume: 0.8,
difficulty: 'normal'
});
return settings;
};
// scenes/gameScene.ts
import { withLocalState, withGlobalState } from 'phaser-hooks';
class GameScene extends Phaser.Scene {
private unsubscribe?: () => void;
create() {
// 1. Local state (scene-specific, auto-cleanup)
const player = withPlayer(this); // clean and reusable within the same scene
// 2. Global state (persists across scenes)
const settings = withSettings(this); // the same instance in all scenes
// 3. Update state
player.patch({ hp: 90 }); // Partial update
settings.set({ volume: 0.5, difficulty: 'hard' }); // Full update
// 4. Read state
console.log(player.get().hp); // 90
console.log(settings.get().volume); // 0.5
// 5. Listen to changes
this.unsubscribe = player.on('change', (newPlayer, oldPlayer) => {
if (newPlayer.hp < 20) {
console.warn(`Low health! Your old HP was ${oldPlayer.hp}`);
}
});
}
shutdown() {
// 6. Clean up (local state auto-cleans, but it’s good practice)
this.unsubscribe?.();
}
}That's it! You now have reactive, type-safe state management in your Phaser game.
Recommended: Create Custom Hooks
Just like in React, the real power comes from creating reusable hooks (but without React):
// GameScene.ts
import { withPlayerState } from './hooks/withPlayerState';
class GameScene extends Phaser.Scene {
create() {
const player = withPlayerState(this); // Clean and reusable!
player.patch({ hp: 90 });
}
}
// HealthBar.ts - Access the SAME state!
class HealthBar extends Phaser.GameObjects.Container {
constructor(scene: Phaser.Scene) {
super(scene, 0, 0);
const player = withPlayerState(scene); // Same state instance!
player.on('change', (newPlayer) => {
this.updateDisplay(newPlayer.hp, newPlayer.maxHp);
});
}
}Key insight: Using the same key returns the same state instance, just like React hooks! This allows you to access state from anywhere: scenes, components, systems, etc.
Initial value: The initial value is only applied during the first execution. On subsequent calls, the same state instance is reused — just like in React Hooks.
Advanced: Hooks with Custom Methods
💡 If you’re not using TypeScript, don’t worry — all hooks work with plain JavaScript too.
However, defining full types for your state object, hook return, and custom methods gives you complete end-to-end type safety with full IntelliSense for every method and return value.
// hooks/withPlayerState.ts
import { withLocalState, type HookState } from 'phaser-hooks';
export type PlayerState = {
hp: number;
maxHp: number;
level: number;
};
export type PlayerHook = HookState<PlayerState> & {
takeDamage: (amount: number) => void;
heal: (amount: number) => void;
levelUp: () => void;
};
const initialPlayerState: PlayerState = {
hp: 100,
maxHp: 100,
level: 1,
};
export function withPlayerState(scene: Phaser.Scene): PlayerHook {
const state = withLocalState<PlayerState>(scene, 'player', initialPlayerState);
const takeDamage = (amount: number): void => {
const current = state.get();
state.patch({
hp: Math.max(0, current.hp - amount),
});
};
const heal = (amount: number): void => {
const current = state.get();
state.patch({
hp: Math.min(current.maxHp, current.hp + amount),
});
};
const levelUp = (): void => {
const current = state.get();
state.patch({
level: current.level + 1,
maxHp: current.maxHp + 10,
hp: current.maxHp + 10,
});
};
return {
...state, // get, set, patch, on, once, off, clearListeners
takeDamage,
heal,
levelUp,
};
}
// Usage in your scene
const player = withPlayerState(this);
console.log(player.get());
player.takeDamage(30);
console.log(player.get());
player.heal(10);
console.log(player.get());
player.levelUp();
console.log(player.get());
/**
* Output:
* {hp: 100, maxHp: 100, level: 1}
* {hp: 70, maxHp: 100, level: 1}
* {hp: 80, maxHp: 100, level: 1}
* {hp: 110, maxHp: 110, level: 2}
*/Next Steps
Core Concepts
Updater Functions
Both set() and patch() accept updater functions for race-condition-safe updates:
// Direct value
player.set({ hp: 90, level: 2 });
// Updater function (recommended when based on current state)
player.set(current => ({ ...current, hp: current.hp - 10 }));
// Patch with updater
player.patch(current => ({ hp: current.hp + 20 }));Why use updater functions? They always work with the latest state, preventing race conditions in async scenarios.
set() vs patch()
set()- Full state replacementpatch()- Partial update with deep merge (only for objects)
const player = withLocalState(this, 'player', {
hp: 100,
maxHp: 100,
level: 1
});
player.set({ hp: 90, maxHp: 100, level: 1 }); // Must provide all properties
player.patch({ hp: 90 }); // Only updates hp, preserves maxHp and levelRule of thumb: Use patch() for object states when you only need to update specific properties.
Debug Mode / Dev tool
Phaser Hooks includes a built-in debug mode that provides detailed logging for state operations. This is extremely useful when developing or debugging state-related issues.
How to Enable Debug Mode
To enable debug mode, simply pass { debug: true } in the options parameter when creating any hook:
import { withLocalState } from 'phaser-hooks';
export const withPlayer = (scene: Phaser.Scene) => {
const playerState = withLocalState<{ hp: number; level: number }>(
this,
'player',
{
hp: 100,
level: 1,
},
{ debug: true }, // Enable debug logging
);
return playerState;
}
// in your scene
playerState.patch((current) => ({ hp: current.hp - 10 })); // Log herePhaser Data Inspector Extension 🔍
For an even better debugging experience, use the Phaser Data Inspector Chrome extension! It provides a visual interface similar to Redux DevTools, allowing you to inspect and debug your Phaser Hooks state in real-time.
Features
- 🎯 Real-Time State Monitoring - Track all state changes from your Phaser Hooks
- 📊 Visual Diff Comparison - See exactly what changed between state updates
- 🔍 Search & Filter - Quickly find specific state keys
- 📄 Event History - Browse through all state changes with pagination
- 🎨 Modern UI - Clean, intuitive interface in Chrome DevTools
Installation
🛒 Download Phaser Data Inspector from Chrome Web Store
Usage
- Install the extension from the Chrome Web Store
- Open Chrome DevTools (F12) on your Phaser game
- Navigate to the "Phaser" tab in DevTools
- All state changes from
phaser-hookswill automatically appear in the inspector
The extension works seamlessly with all Phaser Hooks (withLocalState, withGlobalState, etc.) and provides enhanced debugging capabilities with visual diffs and state history.
Hook API Reference
All hooks return a HookState<T> object with the following methods:
| Method | Description | Parameters | Returns |
|--------|-------------|------------|---------|
| get() | Gets the current state value | None | T - Current state value |
| set(value) | Sets a new state value and triggers change listeners | value: T \| ((current: T) => T) | void |
| patch(value) | Patches object state with partial updates (deep merge) | value: Partial<T> \| ((current: T) => Partial<T>) | void |
| on('change', callback) | Registers a callback for state changes | callback: (newValue: T, oldValue: T) => void | () => void - Unsubscribe function |
| once('change', callback) | Registers a callback that fires only once | callback: (newValue: T, oldValue: T) => void | () => void - Unsubscribe function |
| off('change', callback) | Removes a specific event listener | callback: (newValue: T, oldValue: T) => void | void |
| clearListeners() | Removes all event listeners for this state | None | void |
Notes
set()accepts either a value or an updater function for safe updatespatch()only works with object states and performs deep mergingon()/once()/off()only support the'change'eventoff()requires the exact same function reference that was passed toon()
Example:
Screenshot showing debug logs in the browser console
License
MIT
