ReactでSPAを作る

今回は、Reactでメモアプリ(SPA版)を作成しました。
このアプリはCRUD(一覧表示、詳細閲覧、追加、編集、削除)を実装していて、ページ全体のリロードなしでメモの閲覧、追加、編集、削除が可能になっています。
アロー関数とfunctionが混在していますが、コンポーネントの部分と、イベントハンドラなどの小さい関数を区別するために、コンポーネントの定義にfunction、短いコールバック処理にはアロー関数を使いました。

前提

SPA(Single Page Application)について
  • 1つのHTMLページで完結し、ページ遷移を伴わずにユーザーとの対話が可能なウェブアプリケーションのこと。
  • ページ遷移がないため、スムーズなユーザー体験を提供できる。

公式ドキュメントに書かれているように、Reactを使って1からSPAを開発したい場合、Next.jsやRemixなどのフレームワークを利用することが推奨されている。
今回はReactのみ(フレームワークなし)でプロジェクトを始めることのできるツールであるCreate React Appを使用。

要件

  1. 一覧
    • メモの1行目をタイトルとして一覧表示する。タイトルをクリックするとそのメモの編集状態に移行する。
  2. 詳細
    • 編集状態=詳細
  3. 追加
    • をクリックすると「新規メモ」というメモファイルが作成され、編集状態に移行する。
  4. 編集
    • テキストエリアにメモの内容を表示し、編集できる。編集ボタンをクリックすると保存される。
  5. 削除
    • 編集状態で削除ボタンをクリックするとメモは削除される。そのあとは一覧の状態に移行する。
  6. 保存先ストレージ
    • LocalStorageやFirebaseなど、永続化できれば自由。

アプリの構造

  • 不要なファイルを削ぎ落として、以下のような形に。
memo-app/
├── public/
│   └── index.html
└── src/
    ├── App.js // すべてのメモデータの管理。メモの追加、編集、削除の操作。選択されたメモの管理。
    ├── components/
    │   ├── MemoDetail.js // 選択されたメモの詳細表示。メモの編集と削除。
    │   └── MemoList.js // メモの一覧表示。タイトルクリックで編集モードへの遷移。新規メモの追加。
    ├── hooks/
    │   └── useLocalStorage.js // LocalStorageを用いた永続化の実装。
    ├── index.js // アプリケーションのエントリーポイント。
    └── styles.css
├── .eslintrc.json
├── .gitignore
├── package-lock.json
├── package.json

処理の流れ

1. アプリの起動 (index.js)
  • ReactアプリがDOMにマウントされる。
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
2. メモの一覧表示(App.js, MemoList.js)
<MemoList
    memos={memos}
    onMemoSelect={handleMemoSelect}
    onMemoAdd={handleMemoAdd}
/>
function MemoList({ memos, onMemoSelect, onMemoAdd }) {
  return (
    <>
      {memos.map((memo) => (
        <div
          key={memo.id}
          onClick={() => onMemoSelect(memo.id)}
          className="memo-item"
        >
          {memo.text.split("\n")[0]}
        </div>
      ))}
      <button onClick={onMemoAdd}>+</button>
    </>
  );
}

export default MemoList;
3. 新規メモの追加 (App.js)
  • handleMemoAdd関数で新しいメモが作成され、追加される。
const handleMemoAdd = () => {
  const newMemo = { id: memos.length, text: "新規メモ" };
  setMemos([...memos, newMemo]);
  setSelectedMemoIndex(memos.length);
};
4. メモの選択と詳細表示 (App.js, MemoDetail.js)
<MemoDetail
    memo={memos[selectedMemoIndex]}
    onMemoSave={handleMemoSave}
    onMemoDelete={handleMemoDelete}
/>
5. メモの編集と保存 (MemoDetail.js)
const handleSave = () => {
  onMemoSave({ ...memo, text: memoText });
};
6. メモの削除 (App.js)
  • handleMemoDelete関数でメモが削除される。
const handleMemoDelete = () => {
  const updateMemos = [...memos];
  updateMemos.splice(selectedMemoIndex, 1);
  setMemos(updateMemos);
  setSelectedMemoIndex(null);
};
7. ローカルストレージの利用 (useLocalStorage.js)
  • カスタムフックuseLocalStorageでローカルストレージへの保存と読み込みが行われる。

重要な箇所と使用技術

  • 状態管理にuseStateが使用し、各コンポーネントの内部状態を管理。
  • LocalStorage(useLocalStorageカスタムフック)を使用し、メモを永続化。
  • MemoList.jsの中で、map関数が使用されメモの一覧を表示。
    key属性が重要で、keyindexにしてしまうと、メモを削除したときに同じkeyの値が別のメモに使われることになる。
    これはユニークなIDを使用して回避。

まとめ✏️

最初は「SPAとは??」「巷で噂になってるけど実際どうなんだろう?」と思っていましたが、開発を進める中でページ遷移なしでスムーズなUXを提供できる凄さを実感することができました。
コンポーネント間の状態管理等を学びましたが、ユーザーがどう使うか、どんな体験を感じるかを常に考慮しなければいけないことに気づくことができました。Next.jsも学習するぞ〜