With Persistent State
The withPersistentState hook provides state management with automatic browser storage persistence. Perfect for settings, user preferences, game progress, or any data that should survive page refreshes and browser sessions.
How It Works
Section titled “How It Works”withPersistentState automatically:
- ✅ Loads - Reads from localStorage/sessionStorage on initialization
- ✅ Saves - Writes to storage on every state update
- ✅ Syncs - Keeps state and storage in perfect sync
- ✅ Type-safe - Full TypeScript support with JSON serialization
const settings = withPersistentState(  this,  'settings',  { volume: 0.8, difficulty: 'normal' },  'game-settings', // localStorage key  'local'          // 'local' or 'session');
settings.patch({ volume: 0.5 }); // Automatically saved to localStorage!
// Refresh the page...console.log(settings.get().volume); // 0.5 (loaded from localStorage!)Storage Types
Section titled “Storage Types”localStorage (default)
Section titled “localStorage (default)”Data persists forever (until manually cleared):
const progress = withPersistentState(  this,  'progress',  { level: 1, score: 0 },  'game-progress',  'local' // Persists across browser sessions);sessionStorage
Section titled “sessionStorage”Data persists only for the current tab session:
const tempData = withPersistentState(  this,  'temp',  { currentWave: 1 },  'game-temp',  'session' // Cleared when tab is closed);Creating Custom Hooks
Section titled “Creating Custom Hooks”Create reusable persistent hooks for consistent storage:
import { type HookState, withPersistentState } from 'phaser-hooks';
type Settings = {  volume: number;  musicEnabled: boolean;  sfxEnabled: boolean;  difficulty: 'easy' | 'normal' | 'hard';  language: string;};
type SettingsState = HookState<Settings> & {  toggleMusic: () => void;  toggleSfx: () => void;  setDifficulty: (difficulty: Settings['difficulty']) => void;  adjustVolume: (delta: number) => void;  reset: () => void;};
const DEFAULT_SETTINGS: Settings = {  volume: 0.8,  musicEnabled: true,  sfxEnabled: true,  difficulty: 'normal',  language: 'en',};
export function withSettings(scene: Phaser.Scene): SettingsState {  const state = withPersistentState<Settings>(    scene,    'settings',    DEFAULT_SETTINGS,    'my-game-settings', // localStorage key    'local'  );
  return {    ...state,
    toggleMusic: () => {      const current = state.get();      state.patch({ musicEnabled: !current.musicEnabled });    },
    toggleSfx: () => {      const current = state.get();      state.patch({ sfxEnabled: !current.sfxEnabled });    },
    setDifficulty: (difficulty: Settings['difficulty']) => {      state.patch({ difficulty });    },
    adjustVolume: (delta: number) => {      const current = state.get();      const newVolume = Math.max(0, Math.min(1, current.volume + delta));      state.patch({ volume: newVolume });    },
    reset: () => {      state.set(DEFAULT_SETTINGS);    },  };}const { withPersistentState } = require('phaser-hooks');
const DEFAULT_SETTINGS = {  volume: 0.8,  musicEnabled: true,  sfxEnabled: true,  difficulty: 'normal',  language: 'en',};
function withSettings(scene) {  const state = withPersistentState(    scene,    'settings',    DEFAULT_SETTINGS,    'my-game-settings',    'local'  );
  return {    ...state,
    toggleMusic: () => {      const current = state.get();      state.patch({ musicEnabled: !current.musicEnabled });    },
    toggleSfx: () => {      const current = state.get();      state.patch({ sfxEnabled: !current.sfxEnabled });    },
    setDifficulty: (difficulty) => {      state.patch({ difficulty });    },
    adjustVolume: (delta) => {      const current = state.get();      const newVolume = Math.max(0, Math.min(1, current.volume + delta));      state.patch({ volume: newVolume });    },
    reset: () => {      state.set(DEFAULT_SETTINGS);    },  };}
module.exports = { withSettings };Usage Example: Game Progress
Section titled “Usage Example: Game Progress”import { type HookState, withPersistentState } from 'phaser-hooks';
type GameProgress = {  currentLevel: number;  unlockedLevels: number[];  highScores: Record<number, number>;  achievements: string[];  totalPlayTime: number;};
type GameProgressState = HookState<GameProgress> & {  unlockLevel: (level: number) => void;  setHighScore: (level: number, score: number) => void;  addAchievement: (achievement: string) => void;  incrementPlayTime: (seconds: number) => void;  resetProgress: () => void;};
const INITIAL_PROGRESS: GameProgress = {  currentLevel: 1,  unlockedLevels: [1],  highScores: {},  achievements: [],  totalPlayTime: 0,};
export function withGameProgress(scene: Phaser.Scene): GameProgressState {  const state = withPersistentState<GameProgress>(    scene,    'progress',    INITIAL_PROGRESS,    'my-game-progress',    'local'  );
  return {    ...state,
    unlockLevel: (level: number) => {      const current = state.get();      if (!current.unlockedLevels.includes(level)) {        state.patch({          unlockedLevels: [...current.unlockedLevels, level],        });      }    },
    setHighScore: (level: number, score: number) => {      const current = state.get();      const currentHighScore = current.highScores[level] || 0;
      if (score > currentHighScore) {        state.patch({          highScores: { ...current.highScores, [level]: score },        });      }    },
    addAchievement: (achievement: string) => {      const current = state.get();      if (!current.achievements.includes(achievement)) {        state.patch({          achievements: [...current.achievements, achievement],        });      }    },
    incrementPlayTime: (seconds: number) => {      const current = state.get();      state.patch({ totalPlayTime: current.totalPlayTime + seconds });    },
    resetProgress: () => {      state.set(INITIAL_PROGRESS);    },  };}
// GameScene.tsimport { withGameProgress } from './hooks/withGameProgress';
class GameScene extends Phaser.Scene {  private playTimeTimer?: Phaser.Time.TimerEvent;
  create() {    const progress = withGameProgress(this);
    // Track play time    this.playTimeTimer = this.time.addEvent({      delay: 1000,      callback: () => progress.incrementPlayTime(1),      loop: true,    });
    // Complete level    this.events.on('level-complete', (level: number, score: number) => {      progress.setHighScore(level, score);      progress.unlockLevel(level + 1);      progress.patch({ currentLevel: level + 1 });    });
    // Unlock achievement    this.events.on('achievement', (id: string) => {      progress.addAchievement(id);      this.showAchievementNotification(id);    });  }
  shutdown() {    this.playTimeTimer?.destroy();  }}
// LevelSelectScene.tsclass LevelSelectScene extends Phaser.Scene {  create() {    const progress = withGameProgress(this);    const current = progress.get();
    // Create level buttons    for (let i = 1; i <= 10; i++) {      const isUnlocked = current.unlockedLevels.includes(i);      const highScore = current.highScores[i] || 0;
      const button = this.add.text(100, i * 50, `Level ${i}`, {        color: isUnlocked ? '#ffffff' : '#666666',      });
      if (isUnlocked) {        button.setInteractive();        button.on('pointerdown', () => {          progress.patch({ currentLevel: i });          this.scene.start('GameScene', { level: i });        });      }
      if (highScore > 0) {        this.add.text(300, i * 50, `High Score: ${highScore}`, {          color: '#ffff00',        });      }    }  }}const { withPersistentState } = require('phaser-hooks');
const INITIAL_PROGRESS = {  currentLevel: 1,  unlockedLevels: [1],  highScores: {},  achievements: [],  totalPlayTime: 0,};
function withGameProgress(scene) {  const state = withPersistentState(    scene,    'progress',    INITIAL_PROGRESS,    'my-game-progress',    'local'  );
  return {    ...state,
    unlockLevel: (level) => {      const current = state.get();      if (!current.unlockedLevels.includes(level)) {        state.patch({          unlockedLevels: [...current.unlockedLevels, level],        });      }    },
    setHighScore: (level, score) => {      const current = state.get();      const currentHighScore = current.highScores[level] || 0;
      if (score > currentHighScore) {        state.patch({          highScores: { ...current.highScores, [level]: score },        });      }    },
    addAchievement: (achievement) => {      const current = state.get();      if (!current.achievements.includes(achievement)) {        state.patch({          achievements: [...current.achievements, achievement],        });      }    },
    incrementPlayTime: (seconds) => {      const current = state.get();      state.patch({ totalPlayTime: current.totalPlayTime + seconds });    },
    resetProgress: () => {      state.set(INITIAL_PROGRESS);    },  };}
module.exports = { withGameProgress };Storage Key Naming
Section titled “Storage Key Naming”Choose meaningful storage keys to avoid conflicts:
// ❌ Generic (might conflict with other games)withPersistentState(this, 'settings', defaultSettings, 'settings', 'local');
// ✅ Namespaced (unique to your game)withPersistentState(this, 'settings', defaultSettings, 'my-awesome-game:settings', 'local');
// ✅ Default behavior (auto-namespaced)withPersistentState(this, 'settings', defaultSettings); // Uses 'phaser-hooks-state:settings'Key Features
Section titled “Key Features”| Feature | Description | 
|---|---|
| Auto-save | Writes to storage on every state update | 
| Auto-load | Reads from storage on initialization | 
| localStorage | Persists forever (until manually cleared) | 
| sessionStorage | Persists only for current tab session | 
| JSON serialization | Automatically handles serialization/deserialization | 
| Type-safe | Full TypeScript support | 
Common Use Cases
Section titled “Common Use Cases”User Settings
Section titled “User Settings”const settings = withPersistentState(  this,  'settings',  { volume: 0.8, theme: 'dark' },  'game-settings',  'local');Game Progress
Section titled “Game Progress”const progress = withPersistentState(  this,  'progress',  { level: 1, score: 0 },  'game-progress',  'local');Temporary Session Data
Section titled “Temporary Session Data”const sessionData = withPersistentState(  this,  'session',  { currentWave: 1 },  'game-session',  'session' // Cleared when tab closes);Player Profile
Section titled “Player Profile”const profile = withPersistentState(  this,  'profile',  { name: 'Player', avatar: 'default' },  'game-profile',  'local');Behavior Notes
Section titled “Behavior Notes”JSON Serialization
Section titled “JSON Serialization”Only JSON-serializable data can be persisted:
// ✅ Works (JSON-serializable)withPersistentState(this, 'data', {  number: 42,  string: 'hello',  boolean: true,  array: [1, 2, 3],  object: { nested: true },  null: null,});
// ❌ Won't work (not JSON-serializable)withPersistentState(this, 'data', {  function: () => {},        // Functions lost  date: new Date(),          // Becomes string  regex: /test/,             // Becomes {}  undefined: undefined,      // Removed});Storage Quota
Section titled “Storage Quota”Browsers limit storage size (~5-10MB):
try {  largeState.set(hugeObject);} catch (error) {  console.error('Storage quota exceeded:', error);  // Handle gracefully (clear old data, show warning, etc.)}Privacy Mode
Section titled “Privacy Mode”Some browsers disable storage in private/incognito mode. Always handle gracefully:
// The hook will fall back to in-memory storage if localStorage is unavailableconst settings = withPersistentState(this, 'settings', defaults, 'key', 'local');// Works in private mode, but data won't persistManual Storage Access
Section titled “Manual Storage Access”You can manually inspect or clear storage:
// Check what's storedconsole.log(localStorage.getItem('my-game-settings'));
// Manually clearlocalStorage.removeItem('my-game-settings');
// Clear all game dataObject.keys(localStorage).forEach(key => {  if (key.startsWith('my-game:')) {    localStorage.removeItem(key);  }});When to Use
Section titled “When to Use”✅ Use withPersistentState for:
- User settings and preferences
- Game progress and save data
- High scores and achievements
- Player profiles
- Tutorial/onboarding state
❌ Don’t use withPersistentState for:
- Large binary data (images, audio)
- Sensitive data (passwords, tokens)
- Temporary scene-specific state (use withLocalState)
- Real-time multiplayer state (use backend)
function withPersistentState<T>(  scene: Phaser.Scene,  key: string,  initialValue: T,  storageKey?: string,  storageType?: 'local' | 'session'): HookState<T>Parameters
Section titled “Parameters”- scene- The Phaser scene instance
- key- Unique identifier for the state
- initialValue- Default value (used if no stored value exists)
- storageKey(optional) - Custom localStorage/sessionStorage key (defaults to- phaser-hooks-state:${key})
- storageType(optional) -- 'local'(default) or- 'session'
Returns
Section titled “Returns”A HookState<T> that automatically syncs with browser storage.
See Also
Section titled “See Also”- withLocalState - For temporary scene-specific state
- withGlobalState - For cross-scene state without persistence
- Creating Custom Hooks - Building reusable persistent hooks
Next Steps
Section titled “Next Steps”- Learn about game save systems with persistent state
- Explore settings menu implementation
- See best practices for browser storage