【こだわりTODO】ReactでTODOアプリをちょっとこだわって作ってみた

みなさん、こんにちは。
[Ateam Brides Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-brides)の18日目は@ttkが担当します！
18に関する一言ネタを書こうと思いググってみたところ18は悪魔の数字のようです👿

## 🤔これは何じゃ
この約半年間業務でReactを触ってきて、その間に学んできたことを、TODOに反映させてみました。
機能こそベターですが、設計や型定義などちょっとこだわりをもって作りました。
TODOは言語やフレームワークを学んだときに試しのアプリとして作ることが多く、そこまでこだわって作ることはないかと思いますが、ポイントをお伝えするのには向いているのかなと思い作りました。
まだまだ至らぬ点も多いですが、今後の誰かの参考になると嬉しいです。

## ✨完成形
フォームにTODOを入力したら、未完了のリストに登録されて、完了したらステータスを変更し、不必要になったら削除をすることができるごくごく一般的なTODOです。
スタイルは当たっていません。cssかけるようになりたい、、
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583468/09f994e5-cca5-d245-e694-1b26e8e61b90.gif)

コードはこちらです。
簡易DBとして[JSON Server](https://www.npmjs.com/package/json-server)を使っています。

https://github.com/tatsuki7813/todo-app


## 🧐コードの紹介
開発をする中でちょっとこだわってみた点を上の階層から紹介していきます。
その前に、まずはディレクトリ構成からご紹介します。

```
src
|
+-api
|  |_ todo.js
|
+-components
|  |_ index.ts
|  |_ Todo.tsx
|  |_ Todo
|      |_ CompleteTodos
|         |_ index.ts
|         |_ CompleteTodos.tsx
|         |_ hooks.ts
|      |_ IncompleteTodos
|      |_ Providers
|      |_ TodoInputForm
|
|
+-libs
|  |_ components
|      |_ index.ts
|      |_ TitleLabel
|          |_ index.ts
|          |_ TitleLabel.tsx
|      |_ ...
|  |_ hooks
|      |_ index.ts
|      |_ use-delete-todo.ts
|      |_ ...
|
+-types
   |_ todo.d.ts
```
#### components

`components`にはページを構成するコンポーネントを置いていきます。
基本`『コンポーネントA』フォルダ`の中には`『コンポーネントA』.tsx`と`index.ts`があり必要があれば`hooks.ts`の3ファイルを置きます。
もし、コンポーネントAが子コンポーネントA-1を持つ場合、`『コンポーネントA-1』フォルダ`を`『コンポーネントA』フォルダ`の中に置きます。
今回でいうところのCompleteTodosフォルダやTodoInputFormフォルダがそれに該当します。
Providerについては構成コンポーネントではないので`components`ではないかと思う一方、汎用的に使い回すものでもないので`libs`でもないなとも思い、苦し紛れにここに置いています。
今後いい配置場所を考えていきます。

#### libs
`libs`には`components`内で複数回呼ばれるものをcomponentsとhooksに分けて配置しています。
Formやlabelなど部品として扱うことが多いコンポーネントはここに置くイメージです。

#### index.ts
`index.ts`が何回か出てきたかと思いますが、こちらの中身は以下のようになっています。

```ts
export * from './コンポーネントA';
```
こうすることで、コンポーネントAを呼び出したい時に`hoge/コンポーネントA/コンポーネントA.tsx`と冗長的にかかずに`hoge/コンポーネントA`でインポートをすることができます。
index.tsはコンポーネントに限らず、フォルダに対してやhooksのファイルに対しても使えます。

🏃‍♂️それではコンポーネントの中身を見ていきましょう。


## 🔹Todo.tsx
```jsx:Todo.tsx
import { VFC } from "react";
import { TitleLabel } from "@/libs/components";
import { CompleteTodos } from "./CompleteTodos";
import { IncompleteTodos } from "./IncompleteTodos";
import { TodoInputForm } from "./TodoInputForm";
import { TodoProvider } from "./Providers";

export const Todo: VFC = () => {
  return (
    <TodoProvider>
      <TitleLabel text="TODO管理" tag="h1" />
      <TodoInputForm />
      <IncompleteTodos />
      <CompleteTodos />
    </TodoProvider>
  );
};
```
### 📝POINT: importのエイリアスを設定
本来であればTodo.tsxから見た相対パスで考えると以下のようにかかなければいけません。

```jsx
import { TitleLabel } from "../../libs/components";
```
今回ぐらいならまだしも、もっと階層が深いところからlibsのものをインポートするとなったらパスの指定が大変になってしまいます。
そこで、おすすめしたいのがインポートのエイリアスの設定です。
`craco`というパッケージをインストールして`tsconfig`をいじるだけで簡単に設定ができます。

▼参考にさせていただいたやり方

https://zukucode.com/2021/06/react-create-app-import-alias.html




### 📝POINT: TODOデータと更新関数を配布するTodoProviderでラップ

ページを構成するコンポーネント群が`TodoProvider`でラップされているのがわかるかと思います。
これは内部のコンポーネントのどこからでもTODOリストを取得したり、更新したりできるようにするために設置しています。

```jsx:TodoProvider.tsx
import {
  createContext,
  Dispatch,
  ReactNode,
  SetStateAction,
  useContext,
  useEffect,
  useState,
  VFC,
} from "react";
import { Todo } from "@/types/todo";
import { getAllTodos } from "@/api/todo";

type ContextValue = {
  todos: Todo[];
  setTodos: Dispatch<SetStateAction<Todo[]>>;
};

export const Context = createContext<ContextValue>({} as ContextValue);

type Props = {
  children: ReactNode;
};

export const TodoProvider: VFC<Props> = ({ children }) => {
  const [todos, setTodos] = useState<Todo[]>([]);
  useEffect(() => {
    getAllTodos().then((todos) => setTodos(todos));
  }, []);

  return (
    <Context.Provider value={{ todos, setTodos }}>{children}</Context.Provider>
  );
};

export const useTodos = () => {
  return useContext(Context);
};
```
APIから取得してきたTODOをステートにセットし、そのステート`todos`と、ステートを更新するための関数`setTodos`を`createContext`、`useContext`を使って配布するようにします。
ここでのポイントは必要になったタイミングで`useContext`をするのではなく、このファイル内でカスタムフックとして提供するようにします。
そうすることで毎度useContextとContextをインポートする必要がなく、カスタムフック一つのインポートで済みますし、Contextの名前を気にする必要がなくなるので`Context`として定義できます。


## 🔹TodoInputForm.tsx
```jsx:TodoInputForm
import { VFC } from "react";
import { TitleLabel } from "@/libs/components";
import { useTodoForm } from "./hooks";
import { ErrorMessage } from "@hookform/error-message";

export const TodoInputForm: VFC = () => {
  const { register, handleSubmitTodo, errors } = useTodoForm();

  return (
    <>
      <TitleLabel text="TODO入力" />
      <form onSubmit={handleSubmitTodo}>
        <textarea {...register("content", { required: "This is required." })} />
        <input type="submit" value="追加" />
        <ErrorMessage errors={errors} name="content" />
      </form>
    </>
  );
};
```

### 📝POINT: React Hook Formを利用
フォームまわりの処理は複雑故に何かと実装に時間がかかるものですが、Reactでフォームを実装するときはReact Hook Formを使うと便利です。

https://react-hook-form.com/

フォーム化したいタグにregisterを埋め込むだけでバリデーションを指定できたり、onChangeやonBlurで処理を走らせる、submit処理を楽に行える等、フォーム周りの面倒な処理を請け負ってくれます。
今回はフォームが一つしかなかったのですが、例えば同じフォームを複数箇所で使いたいとった場合などは、ジェネリクスを使って汎用的なフォームコンポーネントをつくることもできます。
React Hook FormはTypeScriptのサポートも充実しており、メソッド等の型も配布してくれています。

https://react-hook-form.com/ts


### 📝POINT: フォームで必要なメソッドはカスタムフックで定義
```ts:hooks.ts
import { Todo } from "@/types/todo";
import { useForm } from "react-hook-form";
import { ulid } from "ulid";
import { useTodos } from "../Providers";
import { addTodo } from "@/api/todo";

export const useTodoForm = () => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<Todo>();
  const { todos, setTodos } = useTodos();

  const handleSubmitTodo = handleSubmit((data: Pick<Todo, "content">) => {
    const newTodo: Todo = { ...data, id: ulid(), done: false };
    addTodo(newTodo).then((addedTodo) => {
      setTodos([addedTodo, ...todos]);
      reset();
    });
  });

  return { register, handleSubmitTodo, errors };
};
```
必要なメソッドや処理は、カスタムフック内で呼び出し、記述しちゃいましょう。
`useForm`でReact Hook Formのメソッドを呼び出しています。ここでちゃんと型定義ができるので安心ですね。
`TodoProvider`のおかげで`todos`と`setTodos`もここで呼び出すことができます。
ビュー側に最低限必要なメソッドやステートだけを渡すことでビュー側の可読性が向上します。

## 🔹CompleteTodos.tsx （IncompleteTodos.tsx）
```jsx:CompleteTodos.tsx
import { VFC } from "react";
import {
  TitleLabel,
  DeleteButton,
  ChangeStatusButton,
} from "@/libs/components";
import { useCompleteTodos } from "./hooks";

export const CompleteTodos: VFC = () => {
  const { completeTodos } = useCompleteTodos();

  return (
    <>
      <TitleLabel text="完了のTODO" />
      <ul>
        {completeTodos.map(({ id, content, done }) => (
          <li key={id}>
            {content}
            <DeleteButton id={id} />
            <ChangeStatusButton id={id} label={done ? "未完了へ" : "完了へ"} />
          </li>
        ))}
      </ul>
    </>
  );
};
```

### 📝POINT: TODOの削除とステータスの更新ボタンをコンポーネント化
`CompleteTodos.tsx`は完了ステータスのTODOを表示させるコンポーネントです。
削除ボタンと未完了へステータスを更新するためボタンはコンポーネント化されています。
そうすることで、このコンポーネントではTODOの削除と更新の関心を持つ必要がなくなり、TODOを表示する役割だけを果たせばよくなります。
また、コンポーネント化することで、例えば削除ボタンが不要になったので消したいといった際は`<DeleteButton id={id} />`を剥がすだけで対応が終わるので、メンテナンスのしやすさにも効果的です。

```jsx:DeleteButton.tsx
import { VFC } from "react";
import { useDeleteTodo } from "@/libs/hooks";
import { Todo } from "@/types/todo";

type Props = {
  id: Todo["id"];
};

export const DeleteButton: VFC<Props> = ({ id }) => {
  const { handleDeleteTodo } = useDeleteTodo();

  return <button onClick={() => handleDeleteTodo(id)}>削除</button>;
};
```
`DeleteButton.tsx`についても考え方は`CompleteTodos.tsx`と同じで、「ここではボタンが押されたらPropsとして受け取ったidを元にTODOを削除します。他のことは知りません。」といった感じで自分の与えられた役割のみを全うするようになっています。

```ts:hooks.ts
import { useTodos } from "@/components/Todo/Providers";
import { deleteTodo } from "@/api/todo";
import { Todo } from "@/types/todo";
import { useCallback } from "react";

export const useDeleteTodo = () => {
  const { todos, setTodos } = useTodos();
  const handleDeleteTodo = useCallback(
    (id: Todo["id"]) => {
      deleteTodo(id).then((deletedTodoId) => {
        const newTodos = todos.filter(({ id }) => id !== deletedTodoId);
        setTodos(newTodos);
      });
    },
    [setTodos, todos]
  );

  return { handleDeleteTodo };
};
```
`useDeleteTodo`内で定義されている`handleDeleteTodo`は`useCallback`でメモ化されています。
`useCallback`でメモ化された関数はpropsとして渡すReact要素が`React.memo`化されているときに力を発揮します。
今回では`button`要素に渡しており、`button`要素は`React.memo`化されていません。
それなのにメモ化した理由は、`handleDeleteTodo`は使われる側のことなんて気にする必要がないからです。
使う側でメモ化したいから、関数もメモ化しようではなく、どんな状況で使われるかは知らないけど、どんな状況でも使えるようにしておくことがポイントです。

## 👨‍💻おわりに
以上、ちょっとこだわってつくったTODOアプリの紹介でした。
こだわったとは言ったものの、まだまだ設計だったり、コードの記述の仕方でもっと最適化できる部分があるかと思います。
今後もReactを触っていくことになりそうなので、より力をつけていけるよう精進して参ります。

本記事を書くにあたり、こちらの本を参考に使わせていただきました。

https://www.amazon.co.jp/%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6-React-Hooks-asakohattori/dp/486354359X

jsの基礎からReactのカスタムフックまで初学者でもとっつきやすく幅広くReactについて学習ができる一冊となっています。
是非読んでみてはいかがでしょうか。

明日の[Ateam Brides Inc. Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ateam-brides)は@mkinさんが担当します。
お楽しみに！！🥳