Next.jsでToDoリストを作りました【CRUD/クラッド/json-server】

JavaScript

Next.jsでTodoアプリを作ったのでメモ。今更ながらCRUD(クラッド)について再認識しています。

アプリではReactでフォームを簡単に作成できるReact Hook Formというライブラリを使っています。そのなかでもフィールドを動的に増減できるuseFieldArryという機能を使いました。

登録したタスクはAPIで更新されることを前提としているので、json-serverを使います。これはAPIモックサーバーが簡単に使用できるライブラリです。
今回はCRUDについて調べたのでjson-server、React Hook Formについてのメモは特になしです。

作成したサンプル

こちらが作成したToDoリストです。右下の「追加する」をクリックすると、タスクが追加されます。
追加したタスクは編集も可能です。完了をチェックすると完了状態になり、完了したタスクは削除することができます。

コード(GitHub)

こちらがコードになります。

GitHub - matsu0314/react-hook-form-todo-sample
Contribute to matsu0314/react-hook-form-todo-sample development by creating an account on GitHub.

アプリ起動方法

ToDoアプリを起動する前に、APIモックサーバーを起動します。

# APIモックサーバーを起動
$ npm run mock:start

ポート3001で起動するので、変更が必要でしたらpackage.jsonの”json-server –watch mock/db.json -p 3001″のポート番号「3001」を好きな番号に変更します。

{
  "name": "workspace",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "mock:start": "json-server --watch mock/db.json -p 3001" ※必要に応じてポート番号変更
  },
・・・・

次にアプリを開発環境で立ち上げます。これでアプリの動作確認ができます。

# アプリを起動
$ npm run dev

読み込むjsonデータ

モックデータとして次の様なjsonファイルを用意します。こちらを初期値としてToDoアプリに表示させます。

{
  "todo": [
    {
      "todoItem": "タスク1",
      "isTodoFinish": false,
      "id": 1
    },
    {
      "todoItem": "タスク2",
      "isTodoFinish": false,
      "id": 2
    },
    {
      "todoItem": "タスク4",
      "isTodoFinish": false,
      "id": 3
    }
  ]
}

アプリのディレクトリ構成

ディレクトリ構成は次の様になっています。※ToDoに関するディレクトリを抜粋しました。
要となるのは、ToDoを表示するコンポーネントです。
具体的にはToDoリスト一覧を表示するTodoListと、タスクの編集状態を表示するEditTodoItemRowです。

.
├── app
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components # コンポーネント
│   └── elements
│       ├── EditTodoItemRow.tsx # タスク編集状態
│       └── TodoList.tsx # Todoリスト
├── mock
│   └── db.json
├── package-lock.json
├── package.json
├── postcss.config.js
└── tsconfig.json

タスクが編集状態の時は、タスクをEditTodoItemRowコンポーネントに切り替えます。タスクを追加する場合は、EditTodoItemRowコンポーネントを追加します。

編集の場合、既存のタスクをEditTodoItemRowコンポーネントに切り替えます。
追加の場合、EditTodoItemRowコンポーネントを追加します。

アプリ一覧を表示するコンポーネント(コード全体)

少しコードが長いですが、こちらがToDoListコンポーネント(TodoList.tsx)です。

編集と新規登録のタスクを表示するコンポーネント(EditTodoItemRow.tsx)がこちらです。

状態管理(State)を設定する

状態管理(State)として、ToDo一覧を管理するtodoItemsと、編集するタスクのIDを管理するeditItemIdを準備します。

  // ToDo一覧を管理(State)
  const [todoItems, setTodoItems] = useState<TodoItem[]>();
  // 編集するタスクのIDを管理(State)
  const [editItemId, setEditItemId] = useState<TodoItem["id"] | "newItem">();

アプリのCRUD

CRUD(クラッド)はアプリの基本機能の用語で、 Create(作成)、Read(読み取り)、Update(更新)、Delete(削除)の頭文字を並べたものになります。

今回のアプリで、この機能に該当するのはこちらです。

Read(読み取り)

Read(読み取り)は、jsonデータ(ToDo一覧)を取得して画面に表示させます。

初回アクセスの際、useEffect()で取得したjsonデータをtodoItems(タスク一覧を管理するState)に格納します。コードはこの部分です。

// jsonを取得してStateに格納
const fetchTodoItems = async () => {
  const { data } = await axios.get(apiURL);
  setTodoItems(data);
};

useEffect(() => {
  // 初期データの取得
  fetchTodoItems();
}, []);

Read(読み取り)は、初回アクセス以外に、Create(作成)、Update(更新)、Delete(削除)でも発生します。ただ、キャンセルで変更のない場合は何もしません。

タスク編集状態の判定

タスクを編集状態にするには、「編集」ボタンをクリックした際、editItemId(State)を自身のタスクIDに変更します。

// 編集をクリックしたらsetEditItemId()を更新する
<button
  type="button"
  disabled={editItemId !== undefined}
  onClick={(e) => {
    setEditItemId(item.id);
  }}
>
  変更
</button>

Stateを更新したらToDo一覧が再描画されます。その際、条件にStateで管理しているIDと一致していたタスクは、EditTodoItemRowコンポーネントを表示して編集状態にします。

{todoItems?.map((item) =>
  editItemId === item.id ? (
    // タスク編集状態
    <EditTodoItemRow />
  ) : (
    // タスク通常
    <tr key={item.id}>
      <td style={{width: "80px"}}>{item.id}</td>
      <td>
        ・・・・
      </td>
    </tr>
  )
)}

Create(作成)・Update(更新)

Create(作成)

ToDo画面の右下の「追加」をクリックすると入力画面が表示されるので、入力して登録します。

Update(更新)

更新は、該当のタスクの「変更」をクリックすると編集モードになります。編集が完了したら登録をクリックします。

こちらがCreate(作成)とUpdate(更新)の処理になります。
フォームを送信した際、CreateとUpdateそれぞれのリクエスト処理を記述しています。

const onSubmit = async (data: TodoItem) => {
  if (item.id) {
    // 更新リクエスト
    await axios.put(`${apiURL}${item.id}`, data);
  } else {
    // 登録(作成)リクエスト
    await axios.post(apiURL, data);
  }
  // 更新していたら一覧を更新してから編集状態を解除
  onCompleted(true);
};

Create(作成)とUpdate(更新)の切り分けは、送信する内容にidがあるかで判断します。
idがない場合、json-server側で登録時に自動採番されます。

onCompleted()は、jsonが更新されていたら、Read(読み取り)を実行してToDo一覧を更新します。
その際、編集タスクのIDを管理している(State)setEditItemIdをリセットして編集状態を解除します。

onCompleted={async (isUpdated) => {
    // 更新していたら一覧を更新してから編集状態を解除
    if (isUpdated) {
      await fetchTodoItems();
    }
    setEditItemId(undefined);
 }}

Delete(削除)

削除は、タスクに完了のチェックが入っている場合だけ可能にしました。

削除ボタンをクリックした時に、タスクが完了になっているのをチェックしてからDelete(削除)のリクエストを送信します。

<button
    type="submit"
    disabled={editItemId !== undefined}
    onClick={async () => {
    if (item.isTodoFinish === false) {
        alert("完了していないタスクは削除できません。");
        return;
    }
    const deleteDialog = confirm(
        `「${item.todoItem}」を削除しますか?`
    );
    if (deleteDialog) {
        await axios.delete(
        `${apiURL}${item.id}`
        );
        await fetchTodoItems();
    }
    }}
    >
    削除
</button>

Delete(削除)リクエストを送信したら、Read(読み取り)を実行してToDoリストを更新します。

以上でCRUDの動きが一通り完了しました!

まとめ

単純なToDoアプリですが、ReactのReact Hook Formを使ってのフォーム作成、zodを使ったバリデーションなど色々なライブラリを使って作成しました。TypeScriptで作成したのでさらに難しかったです。

API必須のPromiseについても、内容を理解しないとうまくタスク更新できないので、今回のアプリは基礎とはいえ改めて勉強になりました、、。