開発日記⑦ 入力内容をブラウザに自動保存する

前回までの進捗

前回は、ロン・ツモ・流局の仮結果を入力し、終局後の点数や順位を確認できるようにした。
これで、オーラスの点数状況を入力して、必要なアガリ点や仮の結果を試せるようになった。
ただし、ページを再読み込みすると、入力した内容がすべて初期状態に戻ってしまう。
点数や席、目標順位などを毎回入力し直すのは面倒なので、今回は入力内容をブラウザに自動保存することにした。

localStorageを使って保存する

今回は、ブラウザに用意されているlocalStorageを使う。
localStorageへ保存したデータは、ページを閉じたり再読み込みしたりしても残る。
サーバーやデータベースを用意する必要がなく、同じブラウザで使う簡単な保存機能なら比較的手軽に実装できる。
今回のアプリでは、次のような情報を保存する。

  • 4人の持ち点
  • 自分の席
  • 目標順位
  • 難易度
  • 本場
  • 供託
  • 終局方法
  • 和了者
  • 放銃者
  • ロン点
  • ツモ時の支払い
  • 流局時のテンパイ者

保存するデータの形は、SavedGameStateという型にまとめた。

type SavedGameState = {
  scores: Scores;
  selfSeat: Seat;
  goal: Goal;
  difficulty: Difficulty;
  honba: number;
  kyotaku: number;
  resultType: ResultType;
  winner: Seat;
  loser: Seat;
  ronPoints: number;
  dealerPayment: number;
  childPayment: number;
  tenpaiSeats: Seat[];
};

保存する項目を型として定義しておくことで、保存時に必要な値が抜けていないか確認しやすくなる。

オブジェクトを文字列に変換する

localStorageへは、オブジェクトをそのまま保存できない。
そこで、JSON.stringifyを使って文字列へ変換する。

export function saveGameState(
  state: SavedGameState,
): void {
  localStorage.setItem(
    storageKey,
    JSON.stringify(state),
  );
}

setItemの第1引数には保存先を識別するキーを、第2引数には保存する文字列を渡す。
保存されたデータは、ブラウザ上では次のような文字列になる。

{
  "selfSeat": "east",
  "goal": "top",
  "honba": 1,
  "kyotaku": 2
}

実際には、点数や仮の終局結果なども含めたデータが保存される。

保存データを読み込む

ページを開いたときは、localStorage.getItemを使って保存済みのデータを取得する。

const savedText =
  localStorage.getItem(storageKey);

まだ一度も保存されていない場合は、nullが返ってくる。
保存データが見つかった場合は、JSON.parseを使って文字列からオブジェクトへ戻す。

const parsed = JSON.parse(
  savedText,
) as SavedGameState;
  • stringify:保存。データを文字列にする
  • parse:読み込み。文字列をデータへ戻す

という関係らしい。

保存に失敗してもアプリを止めない

保存データが壊れていたり、何らかの理由でブラウザへ書き込めなかったりする可能性もある。
そのため、読み込みと保存の処理をtry...catchで囲んだ。

try {
  const savedText =
    localStorage.getItem(storageKey);

  if (savedText === null) {
    return null;
  }

  return JSON.parse(
    savedText,
  ) as SavedGameState;
} catch (error) {
  console.error(
    "保存データの読み込みに失敗しました。",
    error,
  );

  return null;
}

読み込みに失敗した場合は、保存データがないものとしてnullを返す。
保存機能の問題でアプリ全体が表示されなくなるより、初期状態で使える方がよさそう。

初回表示時に保存データを使う

保存データの読み込みは、useGameStateの最初で行う。

const savedState = useMemo(
  () => loadSavedGameState(),
  [],
);

その後、各状態の初期値を決めるときに、保存データがあればそちらを使う。

const [goal, setGoal] =
  useState<Goal>(
    savedState?.goal ??
      initialGameState.goal,
  );

この場合、

  1. 保存された目標順位があれば使う
  2. なければ初期値を使う

という流れになる。
??は、左側がnullまたはundefinedの場合に、右側の値を使う演算子。
保存データの有無によって分岐を書くより、短くまとめられる。
同じ形で、点数、自分の席、本場、供託、終局方法なども復元する。

入力が変わるたびに自動保存する

保存ボタンを用意する方法もあるけれど、今回は入力内容が変わったら自動的に保存する形にした。
ReactのuseEffectを使い、保存対象の値が変更されたときに保存処理を実行する。

useEffect(() => {
  const stateToSave: SavedGameState = {
    scores,
    selfSeat,
    goal,
    difficulty,
    honba,
    kyotaku,
    resultType,
    winner,
    loser,
    ronPoints,
    dealerPayment,
    childPayment,
    tenpaiSeats,
  };

  saveGameState(stateToSave);
}, [
  scores,
  selfSeat,
  goal,
  difficulty,
  honba,
  kyotaku,
  resultType,
  winner,
  loser,
  ronPoints,
  dealerPayment,
  childPayment,
  tenpaiSeats,
]);

配列の中に書いた値のどれかが変わると、保存処理がもう一度実行される。
点数を変更したときも、目標順位を変更したときも、特別な保存操作は必要ない。
利用者から見ると、入力した時点で自動的に保存される。
保存ボタンの押し忘れがなくなる反面、保存対象を追加したときは、この配列への追加も忘れないようにする必要がありそう。

初期状態へ戻す機能も追加する

自動保存されるようになると、間違った入力もそのまま残る。
そこで、保存内容を消して初期状態へ戻す処理も用意した。

function resetSavedState(): void {
  clearSavedGameState();

  setScores({
    ...initialGameState.scores,
  });

  setSelfSeat(
    initialGameState.selfSeat,
  );

  setGoal(
    initialGameState.goal,
  );

  // ほかの状態も初期値へ戻す
}

clearSavedGameStateでは、localStorage.removeItemを使って保存データを削除する。

export function clearSavedGameState(): void {
  localStorage.removeItem(storageKey);
}

その後、Reactで管理している各状態も初期値へ戻す。
状態を初期化すると自動保存の処理が再び動くので、最終的には初期状態の内容が保存される。
これで、ページを再読み込みしても初期状態から始められる。

保存処理を別ファイルに分ける

保存、読み込み、削除の処理は、storage.tsというファイルへまとめた。

storage.ts
├ 保存データを読み込む
├ 現在の状態を保存する
└ 保存データを削除する

Reactの状態管理を行うファイルにlocalStorageの処理をすべて書くこともできる。
ただ、保存方法を後から変更する可能性もあるので、ブラウザへの保存処理は分けておいた。
画面や状態管理側では、

saveGameState(state);

のように呼び出すだけで済む。
保存先を変更するときも、主にstorage.tsを修正すればよくなるはず。

今回できたこと

今回の進捗は以下のとおり。

  • 保存する入力内容を型としてまとめた
  • 入力内容をJSON文字列へ変換した
  • localStorageへデータを保存した
  • ページ表示時に保存データを読み込んだ
  • 保存データがない場合は初期値を使うようにした
  • 入力内容が変わるたびに自動保存した
  • 保存データの読み書きに失敗した場合の処理を追加した
  • 初期状態へ戻す機能を追加した
  • 保存処理を別ファイルへ分離した

これで、ページを再読み込みしても、入力した点数や条件が残るようになった。
見た目の変化は小さいけれど、毎回入力し直す必要がなくなり、かなり使いやすくなったと思う。
ここまでで、点数入力から必要点数の表示、仮の終局結果の判定、入力内容の保存まで一通り動くようになった。
次は、条件を毎回手入力しなくても試せるように、難易度に応じたオーラス条件をランダム生成する機能についてまとめたい。
難易度自体はすでにある(SaveGameStateの項目にある)けど、まだ仮置き状態。

牛一。

コメント

タイトルとURLをコピーしました