Documentation
Save System with localStorage

Save System with localStorage (Including Quick Save)

Overview

NarraLeaf-React does not include a built-in save UI, but provides serialize and deserialize for serializing/deserializing game state. Combined with localStorage (opens in a new tab), you can implement a simple browser-based save system, and use KeyMap for quick save (e.g. Ctrl+S) and quick load (e.g. F9).

This guide demonstrates a full implementation with multiple save slots and quick save/load.

1. Core Save and Load Logic

Use useLiveGame or game.getLiveGame() to get LiveGame, call serialize() to obtain SavedGame, then store it in localStorage:

import { useGame } from "narraleaf-react";
 
function useSaveSystem() {
  const game = useGame();
  const liveGame = game.getLiveGame();
 
  function save(slot: number) {
    const saved = liveGame.serialize();
    // Returns SavedGame: { name, meta, game }
    localStorage.setItem(`save-slot-${slot}`, JSON.stringify(saved));
  }
 
  function load(slot: number) {
    const raw = localStorage.getItem(`save-slot-${slot}`);
    if (!raw) return;
    const saved = JSON.parse(raw);
    liveGame.deserialize(saved);
    // Replaces current game state, triggers stage reset
  }
 
  return { save, load };
}

2. Quick Save and Quick Load

Listen for keydown and call save/load. F5/F9 is a common convention; Ctrl+S is an alternative (check both event.ctrlKey and event.key):

import { useEffect } from "react";
import { useGame } from "narraleaf-react";
 
const QUICK_SAVE_SLOT = 0;  // Slot 0 for quick save/load
 
function useQuickSave() {
  const game = useGame();
  const liveGame = game.getLiveGame();
 
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      // F5: quick save
      if (e.key === "F5") {
        e.preventDefault();
        const saved = liveGame.serialize();
        localStorage.setItem(`save-slot-${QUICK_SAVE_SLOT}`, JSON.stringify(saved));
        liveGame.notify("Saved", 2000);
      }
      // F9: quick load
      else if (e.key === "F9") {
        e.preventDefault();
        const raw = localStorage.getItem(`save-slot-${QUICK_SAVE_SLOT}`);
        if (raw) {
          liveGame.deserialize(JSON.parse(raw));
          liveGame.notify("Loaded", 2000);
        }
      }
      // Ctrl+S: alternative quick save (check both ctrlKey and key)
      else if (e.ctrlKey && e.key === "s") {
        e.preventDefault();
        const saved = liveGame.serialize();
        localStorage.setItem(`save-slot-${QUICK_SAVE_SLOT}`, JSON.stringify(saved));
        liveGame.notify("Saved", 2000);
      }
    };
 
    window.addEventListener("keydown", handler, true);
    return () => window.removeEventListener("keydown", handler, true);
  }, [game]);
}

3. Save Slot UI Example

In a save/load page, list slots and show info from SavedGameMetaData:

function SaveLoadPage() {
  const { save, load } = useSaveSystem();
  const [slots, setSlots] = useState<Record<number, SavedGameMetaData | null>>({});
 
  useEffect(() => {
    const data: Record<number, SavedGameMetaData | null> = {};
    for (let i = 0; i < 5; i++) {
      const raw = localStorage.getItem(`save-slot-${i}`);
      if (raw) {
        try {
          const parsed = JSON.parse(raw);
          data[i] = parsed.meta;  // { created, updated, lastSentence, lastSpeaker }
        } catch {
          data[i] = null;
        }
      } else {
        data[i] = null;
      }
    }
    setSlots(data);
  }, []);
 
  return (
    <div className="grid grid-cols-5 gap-2">
      {[0, 1, 2, 3, 4].map((slot) => (
        <div key={slot} className="border rounded p-2">
          {slots[slot] ? (
            <>
              <p className="text-sm">{new Date(slots[slot]!.updated).toLocaleString()}</p>
              <p className="text-xs truncate">{slots[slot]!.lastSentence}</p>
              <button onClick={() => save(slot)}>Overwrite</button>
              <button onClick={() => load(slot)}>Load</button>
            </>
          ) : (
            <button onClick={() => save(slot)}>Empty - Save</button>
          )}
        </div>
      ))}
    </div>
  );
}

4. Notes

  • Script compatibility: After changing the story script, old saves may not load correctly; see LiveGame docs.
  • Storage limits: localStorage is ~5–10MB; consider IndexedDB (opens in a new tab) or server storage for large saves.
  • Storable and Service: Storable and registered Service (e.g. the built-in Gallery) are saved and restored with serialize/deserialize.

See Also