はじめに
JavaScriptでローカルストレージから複数のアイテムを削除する場合、予期せぬ動作に悩まされることがあります。特にループ内で削除処理を行うと、一部のアイテムが削除されないという問題が発生することがあります。今回はその原因と解決策について解説します。
実際に起きていた問題
私たちのWebアプリケーションでは、ユーザーがフォームに入力したデータをローカルストレージに保存し、「セッション終了」ボタンが押されたときにそれらをクリアする実装をしていました。しかし、以下のような問題が発生していました:
- データが部分的に残存: セッション終了処理後も、一部のフォームデータがローカルストレージに残っていた
- 異なる入力値の混在: 次回アクセス時に前回のセッションの値が一部フォームに表示される
- 不整合なUIの状態: 削除されたはずの値と削除されなかった値が混在し、アプリケーションの状態が不自然になる
- ユーザー混乱: 「クリアしたはず」の値が次回ログイン時に表示され、ユーザーを混乱させる
特に厄介だったのは、この問題が断続的に発生し、再現性が完全ではなかった点です。時にはすべてのデータが正しく削除され、時には一部だけが残るという状況でした。
問題のコード
以下のようなコードを実装していました。正規表現に一致するキーをローカルストレージから削除するものです:
function removeAllItems() {
const regex = /^(form)/; // formで始まるキーを削除
for (let i = 0; i < localStorage.length; i++) {
let keyName = localStorage.key(i);
if (regex.test(keyName)) {
localStorage.removeItem(keyName);
i--; // 削除後にインデックスを調整
}
}
}
このコードでは、削除した後にインデックスを1つ戻す(i--
)ことで、削除によるインデックスのずれを補正しようとしています。しかし、この方法ではすべてのアイテムが確実に削除されるとは限りません。
問題の原因
問題の原因は以下の2点です:
-
ループ中にコレクションが変更される:
localStorage.removeItem()
を呼び出すと、localStorage.length
の値が減少します - インデックスのずれ:アイテムが削除されると、後続のアイテムのインデックス位置が変わります
特に複数のアイテムが連続して削除される場合、i--
による補正では追いつかないケースや、奇妙な挙動が発生する可能性があります。例えば:
- インデックス0と1のアイテムが両方削除対象の場合、0を削除後にインデックス1だったアイテムが0になり、その後iが0に戻って再検査するため正しく動作するように見えます
- しかし、多数のアイテムがある場合や削除パターンによっては、一部のアイテムがチェックされずにスキップされることがあります
解決策
より確実な方法は、削除対象のキーを一度配列に収集してから、別のループで削除するアプローチです:
function removeAllItems() {
const regex = /^(form)/; // formで始まるキーを削除
// 削除するキーをまず配列に収集
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
let keyName = localStorage.key(i);
if (regex.test(keyName)) {
keysToRemove.push(keyName);
}
}
// 収集したキーをすべて削除
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
}
修正後の効果
この修正を適用した結果、以下の改善が見られました:
- 確実なデータ削除: セッション終了時にすべてのフォームデータが確実に削除されるようになった
- クリーンな状態での再開: 次回アクセス時に前回のデータが残らず、新鮮な状態でアプリケーションが起動する
- 一貫したユーザー体験: ユーザーの期待通りの動作になり、混乱が解消された
なぜこの方法がベターなのか
- コレクションの変更が検索に影響しない:まず削除すべきキーのリストを作成するので、元のコレクションを変更せずに全アイテムを走査できます
- 確実に削除できる:収集したキーをすべて削除するので、漏れなく処理できます
- コードの可読性向上:意図が明確になり、バグの可能性が減ります
- 予測可能な振る舞い: 実行の度に同じ結果が得られ、断続的に発生するバグが解消されます
まとめ
このような「ループ中にコレクションを変更する」問題は、localStorage
に限らず、多くの場面で発生する一般的な問題です。配列操作や他のコレクション操作でも同様の問題が起こりえます。
対象の要素をまず特定し、その後で操作するという2段階アプローチは、多くの場合より安全で確実な方法となります。一見小さな実装の違いが、アプリケーション全体の信頼性と安定性に大きな影響を与えることがあることを覚えておきましょう。
教訓: コレクションを走査しながら変更する場合は常に注意が必要です。可能であれば、走査と変更の処理を分離することで、予期せぬ問題を回避できます。