【React】 #5 TODOアプリを作成

# はじめに
現在、Reactを習得するために、23種類のReactアプリ開発に取り組んでいます。
今回はその第五弾として、Next.jsでTodoアプリを作成しました。

https://qiita.com/xYasi/items/0e223b8be80006ef9933

# 要件
一般的なTodoアプリの仕様を満たすことを目標にしました。
具体的には、以下の要件を満たすことを目指しています：

+ タスク登録画面からタスクを登録できること
+ タスク編集画面からタスクを編集できること
+ タスク削除ボタンからタスクを削除できること
+ タスクデータはsupabaseに登録し、データを操作すること

# 作成したアプリのイメージ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3947973/dd558950-5259-4a47-89cc-3b5457a44f6c.jpeg" width=90%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3947973/97c6f262-732a-9bdf-baf3-257b434a31ae.jpeg" width=90%>

# アプリ紹介動画
<iframe width="200" height="450" src="https://www.youtube.com/embed/cv1b5E1V6_k" title="Next Todoアプリ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 利用技術

- Next.js
- TypeScript
- Tailwind CSS
- zod
- prisma
- supabase
- FontAwesome

# 意識したポイント
## SupabaseとPrismaでデータを永続的に保持
データの永続化にはSupabase、データ操作にはPrismaを利用しています。
以下は、タスク削除処理の一部です。
```typescript:features/todos/action.ts
export async function deleteTodoAction(id: string): Promise<TodoState> {
  try {
    await prisma.todo.delete({
      where: {
        id: id,
      },
    });
    revalidatePath("/");
    return {
      error: "",
      success: true,
    };
  } catch (error) {
    if (error instanceof Error) {
      return {
        error: error.message,
        success: false,
      };
    } else {
      return {
        error: "システム異常が発生しました。",
        success: false,
      };
    }
  }
```

フォームのstate管理には、React19で新たに導入された`useActionState`を使用しています。
フォームのアクションをサーバサイドで実行できるため従来より安全性が高いのと、
フォームの実行状態を手軽に取得できるので非常に便利ですね。
```tsx:features/todos/components/TodoDeleteButton.tsx
"use client";
import { useActionState } from "react";
import { deleteTodoAction } from "../lib/action";
import DeleteButton from "@/components/DeleteButton";
import { initialState, TodoState } from "../lib/todoTypes";

type TodoDeleteButtonProps = {
  id: string;
};

const TodoDeleteButton = ({ id }: TodoDeleteButtonProps) => {
  const deleteTodoActionBind = deleteTodoAction.bind(null, id);
  const [state, formAction, pending] = useActionState<TodoState>(
    deleteTodoActionBind,
    initialState
  );
  return (
    <>
      <form action={formAction}>
        <DeleteButton pending={pending} />
      </form>
      {state.error && <p className="text-red-600 mt-2">{state.error}</p>}
    </>
  );
};

export default TodoDeleteButton;

```

SupabaseとPrismaの環境構築に関する詳細は、以下の記事にまとめています。
[【備忘録】Next.js でsupabaseとprismaを利用する](https://qiita.com/xYasi/items/0e8850719b1db83a5b21)

## コンポーネント分割、ディレクトリ構成を意識して実装
CRUD操作が加わったことで、コンポーネントが少し複雑になりました。
今回は実装とルーティングを別々で管理し、
機能に関する実装は`features`ディレクトリにまとめました。
```bash
├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   └── todos
│       ├── [id]
│       │   └── page.tsx
│       └── new
│           └── page.tsx
├── components
│   ├── CancelButton.tsx
│   ├── DeleteButton.tsx
│   ├── Footer.tsx
│   ├── Header.tsx
│   └── SubmitButton.tsx
├── features
│   └── todos
│       ├── components
│       │   ├── TodoCard.tsx
│       │   ├── TodoDeleteButton.tsx
│       │   ├── TodoEditButton.tsx
│       │   ├── TodoEditForm.tsx
│       │   ├── TodoList.tsx
│       │   ├── TodoNewForm.tsx
│       │   └── TodoPageLayout.tsx
│       ├── lib
│       │   ├── action.ts
│       │   └── todoTypes.ts
│       └── types.ts
└── utils
    ├── config.js
    └── prisma.ts
```
この構成は、`Feature-driven Architecture`の概念を参考にしています。
`Feature-driven Architecture`とは、機能を中心に設計・開発を行うアプローチです。
このアプローチにより、役割が明確になり、自分の中でより整理ができたと感じています。

ただし、最近はNext.jsに `dynamic I/O` や `PPR` が導入されることもあり、
app ディレクトリ内に実装をまとめる（コロケートする）べきだとする考え方もあります。

コンポーネント設計については、言語化ができていない部分が多いため、
今後もさまざまな設計手法を学び、アプリに適用していきたいと考えています。

## アイコンは`FontAwesome`を使用
削除、編集、追加ボタンのアイコンにはFontAwesomeを利用しています。
FontAwesomeは無料版でも商用利用が可能で、使っている方は多いと思います。
```bash
# FontAwesomeのインストール
# svg core
npm i --save @fortawesome/fontawesome-svg-core

# flee planのアイコン群
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/free-regular-svg-icons
npm i --save @fortawesome/free-brands-svg-icons

# FontAwesome React コンポーネント
npm i --save @fortawesome/react-fontawesome@latest
```

layout.tsxにインポートの設定をおこないます。
```tsx:layout.tsx
import { config } from "@fortawesome/fontawesome-svg-core";
import "@fortawesome/fontawesome-svg-core/styles.css";
config.autoAddCss = false;
```

インストール後は`FontAwesome`の公式ページにアクセスして
利用したいアイコンのコードをコピーするだけです。

削除ボタンの導入例です。
以下のURLにアクセスしてコードをコピーしてください。

https://fontawesome.com/icons/delete-left?s=solid

```tsx:features/todos/components/TodoDeleteButton.tsx
import { faDeleteLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

const DeleteButton = ({ pending, className = "" }: DeleteButtonProps) => {
  return (
    <button disabled={pending} className={className}>
      <FontAwesomeIcon icon={faDeleteLeft} />
    </button>
  );
};

export default DeleteButton;
```

# Github
GitHubにコードをアップロードしていますので、よかったら確認してみてください。
https://github.com/yasi1989/next-todo

# 参考
https://docs.fontawesome.com/web/use-with/react

https://fontawesome.com