Skip to content

HookState Interface

The HookState<T> interface is the core contract returned by all hooks in Phaser Hooks. It provides a consistent API for getting, setting, and observing state changes.

interface HookState<T> {
get(): T;
set(value: T): void;
on(event: 'change', callback: (newValue: T, oldValue: T) => void): () => void;
once(event: 'change', callback: (newValue: T, oldValue: T) => void): () => void;
off(event: 'change', callback: (newValue: T, oldValue: T) => void): void;
clearListeners(): void;
}

Retrieves the current state value.

get(): T

T - The current state value

const playerState = withLocalState(this, 'player', { hp: 100, level: 1 });
const currentPlayer = playerState.get();
console.log(currentPlayer.hp); // 100
console.log(currentPlayer.level); // 1

Updates the state value and triggers all registered change listeners.

set(value: T): void
ParameterTypeDescription
valueTNew state value
  • ValidationError - If a validator is configured and the value fails validation
const playerState = withLocalState(this, 'player', { hp: 100, level: 1 });
// Update state
playerState.set({ hp: 90, level: 1 });
// Use spread operator for partial updates
playerState.set({
...playerState.get(),
hp: playerState.get().hp - 10,
});
const healthState = withLocalState(this, 'health', 100, {
validator: (value) => {
if (value < 0) return 'Health cannot be negative';
return true;
},
});
try {
healthState.set(-10); // Throws error
} catch (error) {
console.error(error.message); // "Health cannot be negative"
}
healthState.set(90); // Valid - no error

Registers a callback function that fires whenever the state changes.

on(event: 'change', callback: (newValue: T, oldValue: T) => void): () => void
ParameterTypeDescription
event'change'Event type (currently only ‘change’ is supported)
callback(newValue: T, oldValue: T) => voidFunction to call on state change

() => void - Unsubscribe function to remove this listener

const playerState = withLocalState(this, 'player', { hp: 100 });
// Subscribe to changes
const unsubscribe = playerState.on('change', (newPlayer, oldPlayer) => {
console.log('HP changed from', oldPlayer.hp, 'to', newPlayer.hp);
if (newPlayer.hp <= 0) {
console.log('Game Over!');
}
});
playerState.set({ hp: 90 });
// Logs: "HP changed from 100 to 90"
// Later, unsubscribe
unsubscribe();
playerState.set({ hp: 80 });
// No log - listener was removed
const playerState = withLocalState(this, 'player', { hp: 100, mp: 50 });
// First listener - tracks HP
const unsubHP = playerState.on('change', (newPlayer) => {
console.log('Current HP:', newPlayer.hp);
});
// Second listener - tracks MP
const unsubMP = playerState.on('change', (newPlayer) => {
console.log('Current MP:', newPlayer.mp);
});
playerState.set({ hp: 90, mp: 40 });
// Logs both:
// "Current HP: 90"
// "Current MP: 40"
// Unsubscribe individually
unsubHP();
unsubMP();

Registers a callback that fires only once on the next state change, then automatically unsubscribes.

once(event: 'change', callback: (newValue: T, oldValue: T) => void): () => void
ParameterTypeDescription
event'change'Event type (currently only ‘change’ is supported)
callback(newValue: T, oldValue: T) => voidFunction to call on state change

() => void - Unsubscribe function (can be used to cancel before it fires)

const playerState = withLocalState(this, 'player', { level: 1 });
// This will only fire once
playerState.once('change', (newPlayer) => {
console.log('First level up detected!', newPlayer.level);
});
playerState.set({ level: 2 }); // Fires callback
// Logs: "First level up detected! 2"
playerState.set({ level: 3 }); // Does NOT fire callback
// No log
const playerState = withLocalState(this, 'player', { hp: 100 });
const unsubscribe = playerState.once('change', () => {
console.log('This might never fire');
});
// Cancel before it fires
unsubscribe();
playerState.set({ hp: 90 }); // No callback fires

Removes a specific event listener.

off(event: 'change', callback: (newValue: T, oldValue: T) => void): void
ParameterTypeDescription
event'change'Event type (currently only ‘change’ is supported)
callback(newValue: T, oldValue: T) => voidThe exact same function instance passed to on()
export class GameScene extends Phaser.Scene {
private healthCallback: (newPlayer: any, oldPlayer: any) => void;
create() {
const playerState = withLocalState(this, 'player', { hp: 100 });
// Store callback as property
this.healthCallback = (newPlayer, oldPlayer) => {
console.log('HP changed:', newPlayer.hp);
};
// Subscribe
playerState.on('change', this.healthCallback);
// Later, unsubscribe
playerState.off('change', this.healthCallback);
}
}
const playerState = withLocalState(this, 'player', { hp: 100 });
// DON'T DO THIS - won't work!
playerState.on('change', (player) => {
console.log('HP:', player.hp);
});
// This won't remove the listener because it's a different function instance
playerState.off('change', (player) => {
console.log('HP:', player.hp);
});
// INSTEAD, use the unsubscribe function returned by .on():
const unsubscribe = playerState.on('change', (player) => {
console.log('HP:', player.hp);
});
// Later:
unsubscribe(); // This works!

Removes all event listeners for this state.

clearListeners(): void
export class GameScene extends Phaser.Scene {
private playerState: HookState<{ hp: number }>;
create() {
this.playerState = withLocalState(this, 'player', { hp: 100 });
// Add multiple listeners
this.playerState.on('change', () => console.log('Listener 1'));
this.playerState.on('change', () => console.log('Listener 2'));
this.playerState.on('change', () => console.log('Listener 3'));
}
shutdown() {
// Clear all listeners at once
this.playerState.clearListeners();
}
}
  • Scene cleanup - Remove all listeners when transitioning scenes
  • Global state - Essential for cleaning up global state listeners
  • Reset state - Clear all listeners before re-initializing
export class GameScene extends Phaser.Scene {
private globalSettings: HookState<GameSettings>;
create() {
this.globalSettings = withGlobalState(this, 'settings', defaultSettings);
this.globalSettings.on('change', (settings) => {
console.log('Settings updated:', settings);
});
// IMPORTANT: Clean up global state when scene is destroyed
this.events.once('destroy', () => {
this.globalSettings.clearListeners();
});
}
}

import { Scene } from 'phaser';
import { withLocalState } from 'phaser-hooks';
export class GameScene extends Scene {
private playerState: HookState<PlayerData>;
private unsubscribeFns: (() => void)[] = [];
create() {
// Initialize state
this.playerState = withLocalState(this, 'player', {
hp: 100,
maxHp: 100,
level: 1,
exp: 0,
});
// Get current value
const player = this.playerState.get();
console.log('Initial HP:', player.hp);
// Subscribe to changes
const unsubHP = this.playerState.on('change', (newPlayer, oldPlayer) => {
if (newPlayer.hp !== oldPlayer.hp) {
this.updateHealthBar(newPlayer.hp, newPlayer.maxHp);
}
});
this.unsubscribeFns.push(unsubHP);
// One-time listener for first level up
this.playerState.once('change', (newPlayer, oldPlayer) => {
if (newPlayer.level > oldPlayer.level) {
console.log('First level up!');
}
});
// Update state
this.playerState.set({
...this.playerState.get(),
hp: 90,
});
}
updateHealthBar(hp: number, maxHp: number) {
console.log(`HP: ${hp}/${maxHp}`);
}
shutdown() {
// Clean up all listeners
this.unsubscribeFns.forEach(fn => fn());
this.unsubscribeFns = [];
}
}