【JavaScript・localStorage】Webアプリ個人開発で体得したlocalStorageの使い方

# はじめに
これまでやってきたWebアプリ制作でlocalStorageを使用する機会が多かったので
簡単なwebアプリを用いて自分がどのように使ったかを晒します。
ブラウザのローカルストレージ内のデータに期日を設定してそれを過ぎたらデータを更新する
というような機能を実装する際にそこそこ苦労したので
備忘録代わりに残そうと思い筆を執った次第です。
需要があるかどうかはわかりませんが…
技術系の記事を書くのは初めてで読みづらい所があったらごめんなさい。

# 使用する技術
本当はReact.jsで書くのが一番楽だけど分からない人もいると思うので
ここは初心にかえって誰でも分かりそうな HTML + CSS + JavaScript で行こうと思います。

# localStorageについて
簡単にいえばブラウザにデータを保存できる機能です。
詳しいことは[公式ドキュメント](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage)を参照してください。
localStorageの操作はJavaScriptでできます。

データの保存なら **setItem(キー, 値)** というように
```javascript
localStorage.setItem("key", "values");
```
データの取得ならばのように **getItem(キー)** と書き
```javascript
localStorage.getItem("key");
```
データの削除なら **removeItem(キー)**
```javascript
localStorage.removeItem("key");
```
全削除なら
```javascript
localStorage.clear();
```
というような感じで書きます。

ただし、保存できるのは文字列のみなので
それ以外の型は **一旦文字列化して保存** し
**取り出す際に元の型に戻す** 作業が必要になります。

## ブラウザのローカルストレージを確認する方法
Google Chromeだと
その他ツール→デベロッパーツール→Application→Local storageの順で開いていけば確認できます。

![localStorage確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2634870/696cc4a8-81b2-e948-3c2d-1dacd3891b81.png)

chrome以外のブラウザは分からないので
知りたい方は各自で調べてください。

# 今回作るアプリ
簡単なToDoリストを作ります。
inputフォームに入力した内容をページに反映させるシンプルなものです。

![サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2634870/856e8ce2-92d8-cfac-321c-a2dd4e4d97b7.png)

localStorageの使い方の説明がメインなので削除ボタンなどは実装しません。
あくまでタスクを追加する機能だけです。
追加したタスクはリロードしても残る仕様です(もし消したければlocalStorage.clear()等でローカルのデータごと消すしかない)。
タスクの内容と期日を設定して、期日を超過していたら取り消し線が入るという感じのものです(ただし、動的では無いのでページを開いたまま期日が過ぎても反映されず、リロードしないと反映されない)。

## HTML＆CSS
```html:index.html
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      text-align: center;
    }
    
    li {
      list-style: none;
    }

    /* 期日の過ぎたものに取り消し線をする */
    .over {
      text-decoration: line-through;
    }
  </style>
  <title>ToDoリスト</title>
</head>
<body>
  <header>
    <h1>ToDoリスト</h1>
  </header>
  <main>
    <div>
      <form>
        <label for="content">内容</label>:<input id="content" name="content" type="text"><br>
        <label for="date">日付</label>:<input id="date" name="date" type="date"><br>
        <label for="time">時間</label>:<input type="time" name="time" id="time"><br>
        <input type="button" id="button" value="登録">
      </form>
    </div>
    <div id="task-space">
      <!-- ここにpタグでToDoリスト(task)を列挙する -->
    </div>
  </main>
  <script src="main.js"></script>
</body>
</html>
```
### 解説
これといって特筆すべき点はありません。
入力フォームに値を入力しボタンを押してid=”task-space”のdiv内に追加したタスクを表示する流れです。
余談ですが、すること(to do)よりも **任務・課題(task)** のほうが好きなので、変数名などには **task** を付けています。
以降は最小単位を **task** と記していきます(taskの集まりはもちろん **tasks**)。

## JavaScript
```javascript:main.js
// 各入力フォーム、ボタン等の要素を取得
const content = document.getElementById("content")
const date = document.getElementById("date")
const time = document.getElementById("time")
const button = document.getElementById("button")
const taskSpace = document.getElementById("task-space")

// task格納用
let tasks = []

// currentTimeの方が大きければ期日を過ぎたことになる
const currentTime = new Date().getTime()

// taskの表示
const displayTasks = () => {
  let htmlTags = ""
  tasks.map(t => {
    // もし期日を過ぎていたらoverクラスを付けてスタイルに取り消し線を追加する
    htmlTags += `
      <p class=${currentTime >= t.timelimit ? "over" : ""}>${t.content}, ${new Date(t.timelimit)}</p>
    `
  })
  taskSpace.innerHTML = htmlTags
}

// localStorageに存在するかどうかの確認
// 存在すればそのままtasksに格納
const localTasks = localStorage.getItem("local-tasks")

if (!localTasks) {
  // ローカルストレージにtaskが無い時
  console.log("none")
} else {
  // ローカルストレージにタスクがある時
  // ローカルストレージから取り出すときはJSON.parse()
  tasks = JSON.parse(localTasks)
  displayTasks()
}

// unix時間を導き出すための関数
// 入力された日付・時間を結合して数値の配列に変え、返り値にする
const createDatetimeArray = (date, time) => {
  const datetimearr = []
  // dateをハイフン(-)で年・月・時に分割
  const datearr = date.split("-")
  // timeをコロン(:)で時・分に分割
  const timearr = time.split(":")
  // 二つの配列を結合したのち数値に変換→空の配列に格納
  const tmparr = datearr.concat(timearr)
  tmparr.map(t => {
    datetimearr.push(Number(t))
  })
  // 返した配列でunix時間へ変換
  return datetimearr
}

// task追加の関数
const addTask = () => {
  // 入力フォームの存在性チェック
  if (!content.value || !date.value || !time.value) {
    alert("全項目を入力してください")
  } else {
    const timearr = createDatetimeArray(date.value, time.value)
    // taskに格納する際、unix時間にする
    // new Dateに日時を指定する際、月だけ1少ない数じゃないと翌月になる
    const limitdate = new Date(timearr[0], timearr[1]-1, timearr[2], timearr[3], timearr[4])
    // ローカルへ保存前にtasksに格納する
    tasks.push({content: content.value, timelimit: limitdate.getTime()})
    // ローカルストレージに格納するときはJSON.stringify()
    localStorage.setItem("local-tasks", JSON.stringify(tasks))
    displayTasks()
  }
}

// フォームに入力してボタンを押したらtasksに追加
button.addEventListener("click", addTask);
```

## データ型
オブジェクト型の配列です。
local-tasksというキー名で文字列化してローカルに保存します。
timelimitの値は期日で訳あってUNIX時間を使用しており
**この数値が現在の(ページを開いたorリロードした)時間より小さくなれば期日を過ぎたということ** になります。
詳しい解説については[このページ](https://developer.mozilla.org/ja/docs/Glossary/Unix_time)を参照してください。
要するに数値が大きいほど時間が新しい(？)ということです。
普通にDate型でも出来ることですが、理由は後述します。
```javascript
[
  {
    content: "筋トレ",
    timelimit: 1699776000000
  },
  {
    content: "買い物",
    timelimit: 1699779600000
  },
  {
    content: "焼肉パーティ",
    timelimit: 1699866000000
  }
]
```

### 解説
大雑把な流れとしては
1. ページを開いた(リロードした)際にローカルストレージにデータが存在するかどうか確認
2. 存在するなら配列tasksに格納&ページに表示
3. フォームに値を入力
4. 配列tasksに格納
5. ブラウザのローカルストレージに保存
6. ページにtasksの内容を反映

という感じです。

#### 現在の時間の取得
まず、期日を過ぎているか否かの確認をするために必要にな現在の時間を取得して、ます(リロードするたびに更新したいのでグローバル定数にする)。
期日が **currentTime** 以下なら期日が過ぎているということにします。
```javascript
const currentTime = new Date().getTime()
```

#### htmlに表示する関数
先にhtmlにタスクの中身を表示する関数を定義します。
```javascript
const displayTasks = () => {
  let htmlTags = ""
  tasks.map(t => {
    // もし期日が過ぎていたらoverクラスを付けてスタイルに取り消し線を追加する
    htmlTags += `
      <p class=${currentTime >= t.timelimit ? "over" : ""}>${t.content}, ${new Date(t.timelimit)}</p>
    `
  })
  taskSpace.innerHTML = htmlTags
}
```
**currentTime >= t.timelimit ? "over" :""** の部分はif文を省略した[三項演算子](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Conditional_operator)で
現在時間(**currentTime**)が設定された期日(**timelimit**)以上なら期日超過ということで取り消し線が表示される **over** のクラスを付与し
以下ならそのままということです。

#### ローカスストレージ確認
次に、ブラウザのlocalStorageにタスクの値が存在するかの確認を行います。
```javascript
const localTasks = localStorage.getItem("local-tasks")

if (!localTasks) {
  // ローカルストレージにtaskが無い時
  console.log("none")
} else {
  // ローカルストレージにタスクがある時
  // ローカルストレージから取り出すときはJSON.parse()
  tasks = JSON.parse(localTasks)
  displayTasks()
}
```
localStorageに格納されている配列は文字列になっているのでJSON.parse()で元に戻してやります。その後配列tasksに格納してページに内容を反映させます。

#### 入力された期日の処理
入力された期日は日付(**date**)と時間(**time**)に分かれているので、2つの値を配列にして結合し数値に直して返り値にします。
それぞれハイフン( **-** )とコロン( **:** )で分けられているので、**split** で配列にしてやります。
```javascript
const createDatetimeArray = (date, time) => {
  const datetimearr = []
  // dateをハイフン(-)で年・月・時に分割
  const datearr = date.split("-")
  // timeをコロン(:)で時・分に分割
  const timearr = time.split(":")
  const tmparr = datearr.concat(timearr)
  tmparr.map(t => {
    datetimearr.push(Number(t))
  })
  // 返した配列でunix時間へ変換
  return datetimearr
}
```
この作業は新しく追加した **task** を追加する際に期日を設定するために必要です。


#### タスクを追加する関数
まず、if文で各項目のif文で各項目のバリデーション(存在性)チェックします。
バリデーションをクリアしたら、
1. 入力された日付と時間を先ほど定義した関数 **createDatetimeArray** に渡して、数値の配列に加工する
2. 加工し配列をnew Dateにそれぞれ年・月・日・時・分で渡し、Date型のデータを取得する
3. 配列tasksに内容(content)の値とともに **getTime** で **UNIX時間(数値)にしてから** 追加する。
4. ローカルストレージに格納
5. **displayTasks** 関数でページに反映する
という流れです。
```javascript
const addTask = () => {
  if (!content.value || !date.value || !time.value) {
    alert("全項目を入力してください")
  } else {
    const timearr = createDatetimeArray(date.value, time.value)
    // taskに格納する際、unix時間にする
    // 月だけ1少ない数じゃないと翌月になる
    const limitdate = new Date(timearr[0], timearr[1]-1, timearr[2], timearr[3], timearr[4])
    tasks.push({content: content.value, timelimit: limitdate.getTime()})
    // ローカルストレージに格納するときはJSON.stringify()
    localStorage.setItem("local-tasks", JSON.stringify(tasks))
    displayTasks()
  }
}
```
これをボタンのクリックイベントに登録して終わりです。
```javascript
button.addEventListener("click", addTask);
```
codepenで再現してみました。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="wvNeWVK" data-user="syab-syab" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/syab-syab/pen/wvNeWVK">
  ToDoリスト(成功編)</a> by syab-syab (<a href="https://codepen.io/syab-syab">@syab-syab</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## Date型を使わない理由
先ほど期日の超過の有無の確認はDate型でも同じように出来ると書きましたが
このような問題が起きます
新たに **task** を追加した際はこのように問題なく表示されるのですが…
(今回のアプリの仕様上過去の日付も設定できるので追加したばかりでも既に超過している)
![失敗例2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2634870/ec1f3027-1b8f-6ca2-d31d-24acf022f196.png)
再度ページを開きなおしたりリロードすると
![失敗例1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2634870/11012dfe-e53b-9203-f1ca-81198d5d5a9d.png)
このように日本時間からUTCになってしまい、超過の取り消しも消えてしまいます。

原因はローカルに保存するために文字列化(**JSON.JSON.stringify**)すると
日本時間からUTCになってしまうからです。

codepenでも再現しました。
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="xxMrEKz" data-user="syab-syab" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/syab-syab/pen/xxMrEKz">
  Todoリスト(失敗編)</a> by syab-syab (<a href="https://codepen.io/syab-syab">@syab-syab</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

[タイムゾーンを変えずに文字列化する方法](https://qiita.com/querykuma/items/c0c1775e9e8d74b1a378)もあるらしいのです。
個人的にはコードの量が増えるしUNIX時間で比較した方が視覚的・直感的に分かりやすいかなと思ったので
Date型をそのまま使うことはしませんでした。


# まとめ
以上がwebアプリ制作の中で得たノウハウ(？)というほどでもないけどちょっとした知見を反映させたWebアプリを作ってみました。

実は同じような機能を[先日制作したWebアプリ](https://www.syablog.com/2025/06/tasks-maketh-man-list.html)でも実装しました(こちらもToDoリストです)。
あとは毎日決まった時間にデータベースにアクセスして更新する機能を[こちらのアプリ](https://www.syablog.com/2025/06/yenconversion.html)で実装していますが
ちょっと上手く説明できる気がしないので、今のところは止めておきます。
いずれやるかも。
正直、正規の使い方をしているかどうか分からないので、邪道だったらごめんなさい。