With Computed State
The withComputedState hook creates derived state that automatically recomputes when its source state changes. Think of it like Vue’s computed properties or React’s useMemo - perfect for calculations, transformations, and derived values.
How It Works
Section titled “How It Works”withComputedState takes a source state and a selector function, and creates a new state that:
- ✅ Auto-updates - Recalculates whenever the source state changes
- ✅ Read-only - The computed state itself cannot be directly modified
- ✅ Reactive - Triggers its own 'change'listeners when the value changes
- ✅ Type-safe - Full TypeScript inference from source to result
const player = withLocalState(this, 'player', { hp: 100, maxHp: 100 });
// Create computed stateconst healthPercent = withComputedState(  this,  'healthPercent',  player,  (p) => Math.round((p.hp / p.maxHp) * 100));
console.log(healthPercent.get()); // 100
// When source changes, computed updates automaticallyplayer.patch({ hp: 50 });console.log(healthPercent.get()); // 50Creating Custom Hooks with Computed State
Section titled “Creating Custom Hooks with Computed State”Combine computed state with custom hooks for powerful derived values:
import { type HookState, withLocalState, withComputedState } from 'phaser-hooks';
type Player = {  hp: number;  maxHp: number;  mp: number;  maxMp: number;  level: number;  exp: number;  expToNextLevel: number;};
type PlayerState = HookState<Player> & {  healthPercent: HookState<number>;  manaPercent: HookState<number>;  levelProgress: HookState<number>;  isLowHealth: HookState<boolean>;  isDead: HookState<boolean>;  takeDamage: (amount: number) => void;  heal: (amount: number) => void;};
export function withPlayer(scene: Phaser.Scene): PlayerState {  const state = withLocalState<Player>(scene, 'player', {    hp: 100,    maxHp: 100,    mp: 50,    maxMp: 50,    level: 1,    exp: 0,    expToNextLevel: 100,  });
  // Computed values  const healthPercent = withComputedState(    scene,    'player:healthPercent',    state,    (p) => Math.round((p.hp / p.maxHp) * 100)  );
  const manaPercent = withComputedState(    scene,    'player:manaPercent',    state,    (p) => Math.round((p.mp / p.maxMp) * 100)  );
  const levelProgress = withComputedState(    scene,    'player:levelProgress',    state,    (p) => Math.round((p.exp / p.expToNextLevel) * 100)  );
  const isLowHealth = withComputedState(    scene,    'player:isLowHealth',    state,    (p) => p.hp < p.maxHp * 0.25  );
  const isDead = withComputedState(    scene,    'player:isDead',    state,    (p) => p.hp <= 0  );
  return {    ...state,    healthPercent,    manaPercent,    levelProgress,    isLowHealth,    isDead,
    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) });    },  };}const { withLocalState, withComputedState } = require('phaser-hooks');
function withPlayer(scene) {  const state = withLocalState(scene, 'player', {    hp: 100,    maxHp: 100,    mp: 50,    maxMp: 50,    level: 1,    exp: 0,    expToNextLevel: 100,  });
  // Computed values  const healthPercent = withComputedState(    scene,    'player:healthPercent',    state,    (p) => Math.round((p.hp / p.maxHp) * 100)  );
  const manaPercent = withComputedState(    scene,    'player:manaPercent',    state,    (p) => Math.round((p.mp / p.maxMp) * 100)  );
  const levelProgress = withComputedState(    scene,    'player:levelProgress',    state,    (p) => Math.round((p.exp / p.expToNextLevel) * 100)  );
  const isLowHealth = withComputedState(    scene,    'player:isLowHealth',    state,    (p) => p.hp < p.maxHp * 0.25  );
  const isDead = withComputedState(    scene,    'player:isDead',    state,    (p) => p.hp <= 0  );
  return {    ...state,    healthPercent,    manaPercent,    levelProgress,    isLowHealth,    isDead,
    takeDamage: (amount) => {      const current = state.get();      state.patch({ hp: Math.max(0, current.hp - amount) });    },
    heal: (amount) => {      const current = state.get();      state.patch({ hp: Math.min(current.maxHp, current.hp + amount) });    },  };}
module.exports = { withPlayer };Usage Example
Section titled “Usage Example”Use computed values to drive UI and game logic:
import { withPlayer } from './hooks/withPlayer';
class GameScene extends Phaser.Scene {  create() {    const player = withPlayer(this);
    // Create UI elements    const healthBar = this.add.rectangle(10, 10, 200, 20, 0xff0000);    const warningText = this.add.text(10, 40, '', { color: '#ff0000' });
    // Update health bar when percentage changes    player.healthPercent.on('change', (percent) => {      healthBar.width = (200 * percent) / 100;    });
    // Show warning when low health    player.isLowHealth.on('change', (isLow) => {      warningText.setText(isLow ? 'LOW HEALTH!' : '');    });
    // Game over when dead    player.isDead.on('change', (dead) => {      if (dead) {        this.scene.start('GameOverScene');      }    });
    // Simulate taking damage    this.input.keyboard?.on('keydown-SPACE', () => {      player.takeDamage(25);    });  }}
// HealthBar componentclass HealthBar extends Phaser.GameObjects.Container {  private bar: Phaser.GameObjects.Rectangle;  private text: Phaser.GameObjects.Text;
  constructor(scene: Phaser.Scene) {    super(scene, 0, 0);
    const player = withPlayer(scene);
    // Create visuals    this.bar = scene.add.rectangle(0, 0, 200, 20, 0xff0000);    this.text = scene.add.text(0, 25, '', { color: '#ffffff' });
    this.add([this.bar, this.text]);
    // Update on changes    player.healthPercent.on('change', (percent) => {      this.bar.width = (200 * percent) / 100;      this.text.setText(`${player.get().hp} / ${player.get().maxHp}`);    });
    // Initial update    const percent = player.healthPercent.get();    this.bar.width = (200 * percent) / 100;    this.text.setText(`${player.get().hp} / ${player.get().maxHp}`);  }}const { withPlayer } = require('./hooks/withPlayer');
class GameScene extends Phaser.Scene {  create() {    const player = withPlayer(this);
    const healthBar = this.add.rectangle(10, 10, 200, 20, 0xff0000);    const warningText = this.add.text(10, 40, '', { color: '#ff0000' });
    player.healthPercent.on('change', (percent) => {      healthBar.width = (200 * percent) / 100;    });
    player.isLowHealth.on('change', (isLow) => {      warningText.setText(isLow ? 'LOW HEALTH!' : '');    });
    player.isDead.on('change', (dead) => {      if (dead) {        this.scene.start('GameOverScene');      }    });
    this.input.keyboard.on('keydown-SPACE', () => {      player.takeDamage(25);    });  }}Common Use Cases
Section titled “Common Use Cases”Percentages and Progress Bars
Section titled “Percentages and Progress Bars”const player = withLocalState(this, 'player', { hp: 100, maxHp: 100 });
const healthPercent = withComputedState(  this,  'healthPercent',  player,  (p) => (p.hp / p.maxHp) * 100);
healthBar.on('update', () => {  healthBar.setScale(healthPercent.get() / 100, 1);});Boolean Flags
Section titled “Boolean Flags”const isAlive = withComputedState(  this,  'isAlive',  player,  (p) => p.hp > 0);
const canLevelUp = withComputedState(  this,  'canLevelUp',  player,  (p) => p.exp >= p.expToNextLevel);
canLevelUp.on('change', (can) => {  if (can) {    this.showLevelUpButton();  }});Derived Stats
Section titled “Derived Stats”const totalAttack = withComputedState(  this,  'totalAttack',  player,  (p) => p.baseAttack + p.weaponAttack + p.buffAttack);
const critChance = withComputedState(  this,  'critChance',  player,  (p) => Math.min(100, p.luck * 0.5 + p.critBonus));Formatted Strings
Section titled “Formatted Strings”const playerNameLevel = withComputedState(  this,  'playerNameLevel',  player,  (p) => `${p.name} (Lv. ${p.level})`);
playerNameLevel.on('change', (text) => {  nameText.setText(text);});Conditional Logic
Section titled “Conditional Logic”const combatStatus = withComputedState(  this,  'combatStatus',  player,  (p) => {    if (p.hp === 0) return 'dead';    if (p.hp < p.maxHp * 0.25) return 'critical';    if (p.hp < p.maxHp * 0.5) return 'wounded';    return 'healthy';  });
combatStatus.on('change', (status) => {  this.updatePlayerColor(status);});Key Features
Section titled “Key Features”| Feature | Description | 
|---|---|
| Auto-updates | Recomputes when source state changes | 
| Reactive | Emits 'change'events when value changes | 
| Read-only | Cannot directly modify computed state (modify source instead) | 
| Type-safe | TypeScript infers result type from selector | 
| Efficient | Only recalculates when source actually changes | 
Behavior Notes
Section titled “Behavior Notes”Read-Only
Section titled “Read-Only”Computed state is read-only. To change it, modify the source state:
const healthPercent = withComputedState(this, 'hp%', player, p => p.hp / p.maxHp * 100);
healthPercent.set(50); // ❌ Don't do this! (will set the computed value directly)
player.patch({ hp: 50 }); // ✅ Correct - modify source, computed updates automaticallyChange Detection
Section titled “Change Detection”The computed state only emits 'change' when the result value changes, not when the source changes:
const isLowHealth = withComputedState(  this,  'isLowHealth',  player,  (p) => p.hp < 25);
isLowHealth.on('change', () => console.log('Low health status changed!'));
player.patch({ hp: 30 }); // No log (result is still false)player.patch({ hp: 20 }); // Logs! (result changed from false to true)player.patch({ hp: 10 }); // No log (result is still true)Multiple Sources
Section titled “Multiple Sources”For deriving from multiple states, create an intermediate combined state:
const player = withLocalState(this, 'player', { level: 5 });const equipment = withLocalState(this, 'equipment', { weaponDamage: 10 });
// Combine sources firstconst combatStats = withComputedState(  this,  'combatStats',  player,  (p) => ({ playerLevel: p.level, equipment: equipment.get() }));
// Then compute final valueconst totalDamage = withComputedState(  this,  'totalDamage',  combatStats,  (stats) => stats.playerLevel * 2 + stats.equipment.weaponDamage);When to Use
Section titled “When to Use”✅ Use withComputedState for:
- Calculations and percentages
- Boolean flags derived from state
- Formatted strings for display
- Conditional logic based on multiple properties
- Performance-sensitive derived values (computed once, used many times)
❌ Don’t use withComputedState for:
- Simple property access (just use .get())
- Values that change independently of state
- Heavy computations (consider memoization patterns instead)
function withComputedState<T, U>(  scene: Phaser.Scene,  key: string,  sourceState: HookState<T>,  selector: (sourceValue: T) => U): HookState<U>Parameters
Section titled “Parameters”- scene- The Phaser scene instance
- key- Unique identifier for the computed state
- sourceState- The source hook state to derive from
- selector- Function that computes the derived value from source
Returns
Section titled “Returns”A read-only HookState<U> that automatically updates when source changes.
See Also
Section titled “See Also”- withLocalState - For source state
- Creating Custom Hooks - Combining computed with custom hooks
- Event Handling - Reacting to computed changes
Next Steps
Section titled “Next Steps”- Learn about withUndoableState for time-travel
- Explore real-world examples using computed state
- See performance tips for computed values