ローカルストレージを活用したチャット風メモアプリでTypeScriptを学ぼう!

ReactとTypeScriptを使ったアプリ開発、第8回目となる今回は、ブラウザにデータを保存できる「ローカルストレージ」を使ったチャット風メモアプリの作成を通して、TypeScriptの学習をさらに深めていきましょう。

ローカルストレージとは?

WebサイトやWebアプリを使っていると、「前回入力した情報が残っていた」という経験はありませんか? これは「ローカルストレージ」という機能のおかげかもしれません。

ローカルストレージは、ブラウザが提供するデータ保存機能の一つで、Webサイトの情報をユーザーのパソコンやスマートフォンのブラウザに保存できます。

ローカルストレージを使うメリット

ローカルストレージを使うメリットは、大きく分けて以下の2つです。

ブラウザを閉じてもデータが消えない

通常、Webサイトの情報はブラウザを閉じると消えてしまいますが、ローカルストレージに保存されたデータは残ります。そのため、次回同じWebサイトを訪れたときに、前回の状態から再開できます。

オフラインでも利用可能

インターネットに接続していなくても、ローカルストレージに保存されたデータは読み書きできます。これにより、オフライン環境でも一部の機能を利用できるアプリが作成可能です。

今回のチャット風メモアプリでは、メモの内容をローカルストレージに保存することで、ブラウザを閉じたり、誤ってリロードしてしまっても、せっかく書いたメモが消えてしまうのを防ぎます。

ローカルストレージとキャッシュの違いとは?

「ローカルストレージ」と「キャッシュ」は、どちらもWebブラウザにデータを保存する仕組みですが、その目的・性質・使い方はまったく異なります

ローカルストレージとは

ローカルストレージは、開発者が明示的に保存・取得する仕組みです。
たとえば、ユーザーが選んだテーマ(ダークモードなど)やログイン情報などを保存しておきたいときに使います。

jsコピーする編集する// ダークモード設定の保存と取得
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");

特徴としては、「永続的」に保存され、ユーザーがブラウザを閉じてもデータは残ります。

キャッシュとは

キャッシュは、ブラウザやOSが自動的に保存・管理するデータです。
画像やCSS、JavaScriptなどのファイルをブラウザが保存しておくことで、再読み込みを高速化します。

また、開発者がService Workerなどを使ってAPIレスポンスやページ全体をキャッシュすることも可能です。


項目ローカルストレージキャッシュ
📌 用途ユーザー設定・状態の保存高速表示・再取得の回避
🧠 管理者開発者がコードで制御ブラウザ/OSが自動で管理(一部制御可)
⏳ 保存期間明示的に削除するまで永続短期的(容量・期限で自動削除)
💾 容量目安5〜10MB数MB〜数百MB(変動あり)
🧰 主な用途テーマ設定、トークン保存、ログイン状態保持など画像・CSS・APIレスポンスの一時保存
🔐 アクセス制限同一オリジンでのみ可能同様にオリジン制限ありだが自動的に消える可能性あり

【実践】ローカルストレージを活用したメモアプリを作成しよう

それでは、実際にReactとTypeScriptを使って「チャット風メモアプリ」を作成していきましょう。このアプリでは、以下の機能を実現します。

  • 入力フォームにテキストを入力して送信できる
  • 送信されたメモがチャットのように下から上に積み上がる
  • 作成したメモをローカルストレージに保存し、ブラウザを閉じても残る
  • 不要なメモを削除できる

ステップ1:プロジェクトの作成

まずは、React + TypeScript の開発環境を整えましょう。以下のコマンドを実行してください。

npx create-react-app ait-chatmemo-app8 --template typescript
cd ait-chatmemo-app8
npm start

これらのコマンドを実行すると、Reactプロジェクトが作成され、開発サーバーが起動します。

ブラウザで http://localhost:3000 が表示されれば準備OKです!

VScode上では、以下のようなファイル構造が作成されています。

chat-memo-app/
├── public/
├── src/
│   ├── components/
│   │   ├── MessageList.tsx       ← メッセージ一覧表示
│   │   └── MessageInput.tsx      ← 入力フォーム
│   ├── types/
│   │   └── Message.ts            ← メッセージ型定義
│   ├── App.tsx                   ← 全体構成
│   ├── App.css                   ← スタイル
│   └── index.tsx
├── package.json
└── tsconfig.json

各ファイルの役割

ファイル名役割
MessageInput.tsx入力欄と「送信」ボタン
MessageList.tsx送られたメモ(チャット)一覧表示
Message.tsinterface Message { text: string; time: Date } の型定義
App.tsxメイン画面構成・状態管理

ステップ2:必要なフォルダ構成を作る

次に、コードを整理するために必要なフォルダとファイルを作成し、不要な初期ファイルを削除します。

mkdir src/components
mkdir src/types
touch src/components/MessageInput.tsx
touch src/components/MessageList.tsx
touch src/types/Message.ts

不要な初期ファイルを削除する

下記コマンドで不要な初期ファイルを削除します。

rm src/App.test.tsx src/index.css src/logo.svg src/reportWebVitals.ts src/setupTests.ts

ステップ3:メモアプリ用のコードを記述する

それでは、実際にメモアプリのコードを記述していきましょう。

src/types/Message.ts

まず、メモのデータ構造を定義します。

TypeScriptでは、データの「型」を定義することで、コードの可読性を高め、エラーを防ぐことができます。

export interface Message {
  id: number;
  text: string;
  time: string;
}

ここでは、Messageという名前のインターフェースを定義しています。idはメモを識別するための一意な番号、textはメモの内容、timeはメモが作成された時間を表します。

src/components/MessageInput.tsx

次に、メモの入力フォームを作成します。

import React, { useState } from "react";

interface Props {
  onSend: (text: string) => void;
}

const MessageInput: React.FC<Props> = ({ onSend }) => {
  const [text, setText] = useState("");

  const handleSend = () => {
    if (text.trim()) {
      onSend(text);
      setText("");
    }
  };

  return (
    <div className="input-area">
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="メモを入力..."
      />
      <button onClick={handleSend}>送信</button>
    </div>
  );
};

export default MessageInput;

src/components/MessageList.tsx

続いて、送信されたメモの一覧を表示するコンポーネントを作成します。

import React from "react";
import { Message } from "../types/Message";

interface Props {
  messages: Message[];
}

const MessageList: React.FC<Props> = ({ messages }) => {
  return (
    <div className="message-list">
      {messages.map((msg) => (
        <div key={msg.id} className="message">
          <p>{msg.text}</p>
          <span className="time">{msg.time}</span>
        </div>
      ))}
    </div>
  );
};

export default MessageList;

このコンポーネントは、messagesというプロパティを通じて、Message型の配列を受け取ります。

受け取ったメモの配列をmap関数で繰り返し処理し、一つ一つのメモを画面に表示しています。

src/App.tsx

アプリ全体の構成と状態管理を行うメインコンポーネントです。

import React, { useState } from "react";
import "./App.css";
import { Message } from "./types/Message";
import MessageInput from "./components/MessageInput";
import MessageList from "./components/MessageList";

const App: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);

  const handleSend = (text: string) => {
    const newMessage: Message = {
      id: Date.now(),
      text,
        time: new Date().toLocaleString("ja-JP", {
        year: "numeric",
        month: "long",
        day: "numeric",
        hour: "2-digit",
        minute: "2-digit",
        second: "2-digit",
      }),
    };
    setMessages((prev) => [newMessage, ...prev]);
  };

  return (
    <div className="App">
      <h1>チャット風メモアプリ</h1>
      <MessageInput onSend={handleSend} />
      <MessageList messages={messages} />
    </div>
  );
};

export default App;

ここでは、useStateを使ってmessagesというステート(状態)を管理しています。

src/App.css

アプリの見た目を整えるためのスタイルシートです。

.App {
  max-width: 500px;
  margin: 0 auto;
  padding: 2rem;
  font-family: sans-serif;
  text-align: center;
}

.input-area {
  display: flex;
  gap: 8px;
  margin-bottom: 1rem;
}

.input-area input {
  flex: 1;
  padding: 0.5rem;
  font-size: 1rem;
}

.input-area button {
  padding: 0.5rem 1rem;
  font-size: 1rem;
}

.message-list {
  display: flex;
  flex-direction: column-reverse;
  gap: 12px;
  max-height: 400px;
  overflow-y: auto;
}

.message {
  background: #f0f0f0;
  padding: 0.75rem;
  border-radius: 8px;
  text-align: left;
}

.message .time {
  display: block;
  font-size: 0.8rem;
  color: #666;
  margin-top: 4px;
}

src/index.tsx

通常、create-react-appによって自動で生成されていますが、念のため内容を確認しておきましょう。

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
    <App />
);

ステップ4:ブラウザでアプリを確認

ここまでのコードで、基本的なチャット風メモアプリが動作するはずです。開発サーバーが起動している状態でブラウザを確認してみましょう。

しかし、この時点では、メモを入力して「送信」しても、ブラウザを更新するとせっかく書いたメモが消えてしまいます

ステップ5:ブラウザ更新後もメモが表示されるように改良

ブラウザを更新するとメモが消えてしまう問題を解決するため、ここでローカルストレージの出番です!

App.tsxを修正して、メモをローカルストレージに保存する機能を追加しましょう。

App.tsxを以下の内容に変更します。

import React, { useState, useEffect } from "react";
import "./App.css";
import { Message } from "./types/Message";
import MessageInput from "./components/MessageInput";
import MessageList from "./components/MessageList";


const App: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);

    // 初回マウント時に localStorage からデータを取得
    useEffect(() => {
      const stored = localStorage.getItem("messages");
      if (stored) {
        setMessages(JSON.parse(stored));
      }
    }, []);
  
    // メッセージが更新されるたびに localStorage に保存
    useEffect(() => {
      console.log("保存対象:", messages);
      try {
        localStorage.setItem("messages", JSON.stringify(messages));
      } catch (e) {
        console.error("保存エラー", e);
      }
    }, [messages]);
    
  
  const handleSend = (text: string) => {
    const newMessage: Message = {
      id: Date.now(),
      text,
      time: new Date().toLocaleString("ja-JP", {
        year: "numeric",
        month: "long",
        day: "numeric",
        hour: "2-digit",
        minute: "2-digit",
        second: "2-digit",
      })
      ,
    };
    setMessages((prev) => [newMessage, ...prev]);
  };

  return (
    <div className="App">
      <h1>チャット風メモアプリ</h1>
      <MessageInput onSend={handleSend} />
      <MessageList messages={messages} />
    </div>
  );
};

export default App;

1行目を以下に修正しています。
import React, { useState, useEffect } from “react”;

また、2つの useEffect ブロックを追加しました。

これで、ブラウザを更新してもメモが消えずに表示されるようになりました!

実際にメモをいくつか入力して、ブラウザを更新して試してみてください。

ステップ6:メモ削除ボタンの追加

メモを保存できるようになったのは便利ですが、間違って入力したり、不要になったメモを削除できないのは不便です。

最後に、メモをすべて削除するボタンを追加しましょう。

画像の赤枠のVScode内にあるファイル作成ボタンを押して、componentsフォルダ内にClearButton.tsxファイルを作成します。

src/components/ClearButton.tsxの内容は以下の通りです。

import React from "react";

interface Props {
  onClear: () => void;
}

const ClearButton: React.FC<Props> = ({ onClear }) => {
  return (
    <button onClick={onClear} className="clear-button">
      すべて削除
    </button>
  );
};

export default ClearButton;

このボタンは、クリックされると親コンポーネントから渡されたonClear関数を実行します。

App.tsxを編集

次に、App.tsxを修正して、このClearButtonを組み込みます。

App.tsxの6行目に以下を追加します。

import ClearButton from "./components/ClearButton";

App.tsxのhandleSend関数の後に、handleClear関数を追加します。

  const handleClear = () => {
    if (window.confirm("本当にすべて削除しますか?")) {
      setMessages([]);
      localStorage.removeItem("messages");
    }
  };

最後に、App.tsxのreturn文内のMessageListコンポーネントの下に、ClearButtonを追加します。

<ClearButton onClear={handleClear} />

これで、アプリに「すべて削除」ボタンが追加されました。ブラウザでアプリを開き、ボタンをクリックしてみましょう。

「本当にすべて削除しますか?」というポップアップが表示され、確認後にメモがすべて削除されるはずです。

まとめ

今回は、ReactとTypeScriptを使ってチャット風メモアプリを作成しながら、ローカルストレージへのデータ保存と取得、そして削除の方法を学びました。

  • ローカルストレージは、ブラウザにデータを永続的に保存できる機能です。
  • localStorage.setItem()でデータを保存し、localStorage.getItem()でデータを取得します。
  • localStorage.removeItem()でデータを削除できます。
  • JSON.stringify()でJavaScriptのオブジェクトをJSON文字列に変換し、JSON.parse()でJSON文字列をJavaScriptのオブジェクトに変換します。

ローカルストレージは、簡単なデータ保存には非常に便利ですが、大量のデータや機密性の高いデータの保存には向きません。そのような場合は、データベースやサーバーサイドのストレージを利用することを検討してください。

今回の学習を通して、ReactとTypeScriptの組み合わせで、より実用的なWebアプリケーションが作成できることが実感できたのではないでしょうか。これからも様々な機能を実装しながら、さらなるスキルアップを目指していきましょう!

プロフィール
tanat

「株式会社あいてぃ」に所属。
主にAWSとAzureなどのクラウドインフラを管理するクラウドエンジニアとして活動中。
スマホアプリやフロント部分の構築も可能なフルスタックエンジニアとして活躍できるように日々勉強中です。

tanatをフォローする
プログラミング
tanatをフォローする

コメント

タイトルとURLをコピーしました
function cocoon_custom_code_copy_script() { ?>   } add_action('wp_footer', 'cocoon_custom_code_copy_script');