基礎から学ぶReact/React Hooks学習メモ 6-2 TODOアプリの実装

# 6-2 TODOアプリの実装

参考
[基礎から学ぶReact/React Hooks](https://www.amazon.co.jp/dp/B09CP8T4TX)

## モックサーバーと通信してTODOを取得する

- json-serverの起動

```json:db.json
{
  "todos": [
    {
      "id": 1,
      "content": "Create react appをインストールする",
      "done": true
    },
    {
      "id": 2,
      "content": "JSON Server仮のAPIを作成する",
      "done": false
    },
    {
      "id": 3,
      "content": "Chakra UIをインストールする",
      "done": false
    }
  ]
}
```

```sh
json-server --watch db.json --port 3100
```

## TODOアプリの作成（リスト取得のみ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/c3c8c587-1aab-c1c3-0240-e4dc3c4f70d8.png)


```js:src\components\App.js
import React, { useState, useEffect } from "react";

import axios from "axios";

const todoDataUrl = "http://localhost:3100/todos";

function App() {
  const [todoList, setTodoList] = useState([]);

  // useEffectで、コンポーネントのマウント後に処理を実行
  useEffect(() => {
    // 非同期処理
    const fetchData = async () => {
      const response = await axios.get(todoDataUrl);
      setTodoList(response.data);
    };
    fetchData();
  }, []);

  console.log("TODOリスト:", todoList);

  return (
    <>
      <h1>TODO進捗管理</h1>
      <textarea />
      <button>+ TODOを追加</button>

      <h2>TODOリスト</h2>
      <ul>
        {todoList.map((todo) => (
          <li key={todo.id}>
            {todo.content}({todo.done ? "完了" : "未完了"})
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
```

## TODO一覧を状態別に表示

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/22582230-c499-cb14-7860-f153051b6023.png)

- todoListをdoneのture/falseによって、incompletedListとcompletedListに分ける

```js:src\components\App.js
import React, { useState, useEffect } from "react";

import axios from "axios";

const todoDataUrl = "http://localhost:3100/todos";

function App() {
  const [todoList, setTodoList] = useState([]);

  // useEffectで、コンポーネントのマウント後に処理を実行
  useEffect(() => {
    // 非同期処理
    const fetchData = async () => {
      const response = await axios.get(todoDataUrl);
      setTodoList(response.data);
    };
    fetchData();
  }, []);

  console.log("TODOリスト:", todoList);

  // 未完了リスト
  const incompletedList = todoList.filter((todo) => !todo.done);
  // 完了リスト
  const completedList = todoList.filter((todo) => todo.done);

  return (
    <>
      <h1>TODO進捗管理</h1>
      <textarea />
      <button>+ TODOを追加</button>

      <h2>未完了TODOリスト</h2>
      <ul>
        {incompletedList.map((todo) => (
          <li key={todo.id}>
            {todo.content}
            <button>未完了リストへ</button>
            <button>削除</button>
          </li>
        ))}
      </ul>

      <h2>完了TODOリスト</h2>
      <ul>
        {completedList.map((todo) => (
          <li key={todo.id}>
            {todo.content}
            <button>未完了リストへ</button>
            <button>削除</button>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;
```

## タイトルとTODOリストをコンポーネント化する

- TodoTitle
    - h1やh2の大きな文字を表示する
- TodoItem
    - 1つのTodo、内容と移動・削除ボタン
- TodoList
    - TodoItemをループして表示

```js:src\components\App.js
import React, { useState, useEffect } from "react";

import axios from "axios";

const todoDataUrl = "http://localhost:3100/todos";

// タイトルの表示コンポーネント
const TodoTitle = ({ title, as }) => {
  if (as === "h1") {
    return <h1>{title}</h1>;
  } else if (as === "h2") {
    return <h2>{title}</h2>;
  } else {
    return <p>{title}</p>;
  }
};

// 1つのTodo、内容と移動・削除ボタン
const TodoItem = ({ todo }) => {
  return (
    <>
      {todo.content}
      <button>{todo.done ? "未完了リストへ" : "完了リストへ"}</button>
      <button>削除</button>
    </>
  );
};

// TodoItemをループして表示
const TodoList = ({ todoList }) => {
  return (
    <ul>
      {todoList.map((todo) => (
        <li key={todo.id}>
          <TodoItem todo={todo} />
        </li>
      ))}
    </ul>
  );
};

function App() {
  const [todoList, setTodoList] = useState([]);

  // useEffectで、コンポーネントのマウント後に処理を実行
  useEffect(() => {
    // 非同期処理
    const fetchData = async () => {
      const response = await axios.get(todoDataUrl);
      setTodoList(response.data);
    };
    fetchData();
  }, []);

  console.log("TODOリスト:", todoList);

  // 未完了リスト
  const incompletedList = todoList.filter((todo) => !todo.done);
  // 完了リスト
  const completedList = todoList.filter((todo) => todo.done);

  return (
    <>
      <TodoTitle title="TODO進捗管理" as="h1" />
      <textarea />
      <button>+ TODOを追加</button>
      <TodoTitle title="未完了TODOリスト" as="h2" />
      <TodoList todoList={incompletedList} />
      <TodoTitle title="完了TODOリスト" as="h2" />
      <TodoList todoList={completedList} />
    </>
  );
}

export default App;
```

## サーバと通信するtodo.jsの作成

```js:src\apis\todos.js
import axios from "axios";

const todoDataUrl = "http://localhost:3100/todos";

// 全TODOリスト取得
export const getAllTodosData = async () => {
  const response = await axios.get(todoDataUrl);
  return response.data;
};

// 1件のTODOを追加する
export const addTodoData = async (todo) => {
  const response = await axios.post(todoDataUrl, todo);
  return response.data;
};

// 1件のTODOを削除する
export const deleteTodoData = async (id) => {
  await axios.delete(`${todoDataUrl}/${id}`);
  return id;
};

// 1件のTODOを更新する
export const updateTodoData = async (id, todo) => {
  const response = await axios.put(`${todoDataUrl}/${id}`, todo);
  return response.data;
};
```

## TODOを取得、追加、更新、削除するカスタムフックを作成する

```js:src\hooks\useTodo.js
import React, { useState, useEffect } from "react";
import { ulid } from "ulid";

import * as todoData from "../apis/todos";

export const useTodo = () => {
  const [todoList, setTodoList] = useState([]);

  useEffect(() => {
    todoData.getAllTodosData().then((todo) => {
      console.log(todo);
      setTodoList([...todo].reverse());
    });
  }, []);

  // todoのdoneを反転させる
  const toggleTodoListItemStatus = (id, done) => {
    // todoListから、idが一致する1件を取り出す
    const todoItem = todoList.find((item) => item.id === id);
    // doneを反転させて、新たなitemを作成
    const newTodoItem = { ...todoItem, done: !done };
    // サーバに更新API呼ぶ
    todoData.updateTodoData(id, newTodoItem).then((updatedTodo) => {
      // 成功したら、todoListを更新。idが一致しているものを、サーバーから返ってきたupdatedTodoで更新する
      const newTodoList = todoList.map((item) => (item.id !== updatedTodo.id ? item : updatedTodo));
      // 新しいtodoListをstateにセットする
      setTodoList(newTodoList);
    });
  };

  const addTodoListItem = (todoContent) => {
    // あたらしいitemを作成する
    const newTodoItem = { id: ulid, content: todoContent, done: false };
    // サーバーの追加APIを呼ぶ
    todoData.addTodoData(newTodoItem).then((addTodo) => {
      // addTodoをtodoListに追加してstateにセットする
      setTodoList([addTodo, ...todoList]);
    });
  };

  const deleteTodoListItem = (id) => {
    // サーバーの削除APIを呼ぶ
    todoData.deleteTodoData(id).then((deletedid) => {
      const newTodoList = todoList.filter((item) => item.id !== deletedid);
      // 1件削除された新しいtodoListに追加してstateにセットする
      setTodoList(newTodoList);
    });
  };

  // 作成した関数を返す
  return { todoList, toggleTodoListItemStatus, addTodoListItem, deleteTodoListItem };
};
```

## 完成したTODOリスト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/76453e99-920b-99a5-05af-cbb823267562.png)

```
src/
┣ apis/
┃ ┗ todos.js
┣ components/
┃ ┣ App.js
┃ ┣ TodoAdd.js
┃ ┣ TodoItem.js
┃ ┣ TodoList.js
┃ ┗ TodoTitle.js
┣ hooks/
┃ ┗ useTodo.js
┗ index.js
```

```js:src\apis\todos.js
import axios from "axios";

const todoDataUrl = "http://localhost:3100/todos";

// 全TODOリスト取得
export const getAllTodosData = async () => {
  const response = await axios.get(todoDataUrl);
  return response.data;
};

// 1件のTODOを追加する
export const addTodoData = async (todo) => {
  const response = await axios.post(todoDataUrl, todo);
  return response.data;
};

// 1件のTODOを削除する
export const deleteTodoData = async (id) => {
  await axios.delete(`${todoDataUrl}/${id}`);
  return id;
};

// 1件のTODOを更新する
export const updateTodoData = async (id, todo) => {
  const response = await axios.put(`${todoDataUrl}/${id}`, todo);
  return response.data;
};
```

```js:src\components\App.js
import React, { useRef } from "react";

import { useTodo } from "../hooks/useTodo";
import { TodoAdd } from "./TodoAdd";
import { TodoList } from "./TodoList";
import { TodoTitle } from "./TodoTitle";

function App() {
  const { todoList, toggleTodoListItemStatus, addTodoListItem, deleteTodoListItem } = useTodo();

  const inputEl = useRef(null);

  const handleAddTodoListItem = () => {
    if (inputEl.current.value === "") {
      return;
    }
    addTodoListItem(inputEl.current.value);
    inputEl.current.value = "";
  };

  console.log("TODOリスト:", todoList);

  // 未完了リスト
  const incompletedList = todoList.filter((todo) => !todo.done);
  // 完了リスト
  const completedList = todoList.filter((todo) => todo.done);

  return (
    <>
      <TodoTitle title="TODO進捗管理" as="h1" />
      <TodoAdd buttonText="+ TODOを追加" inputEl={inputEl} handleAddTodoListItem={handleAddTodoListItem} />
      <TodoList todoList={incompletedList} toggleTodoListItemStatus={toggleTodoListItemStatus} deleteTodoListItem={deleteTodoListItem} title="未完了TODOリスト" as="h2" />
      <TodoList todoList={completedList} toggleTodoListItemStatus={toggleTodoListItemStatus} deleteTodoListItem={deleteTodoListItem} title="完了TODOリスト" as="h2" />
    </>
  );
}

export default App;
```

```js:src\components\TodoAdd.js
export const TodoAdd = ({ buttonText, inputEl, handleAddTodoListItem }) => {
  return (
    <>
      <textarea ref={inputEl} />
      <button onClick={handleAddTodoListItem}>{buttonText}</button>
    </>
  );
};
```

```js:src\components\TodoItem.js
// 1つのTodo、内容と移動・削除ボタン
export const TodoItem = ({ todo, toggleTodoListItemStatus, deleteTodoListItem }) => {
  // onClickイベントが発生したら、useTodoフックを呼び出す
  const handleToggleTodoListItemStatus = () => toggleTodoListItemStatus(todo.id, todo.done);
  const handleDeleteTodoListItem = () => deleteTodoListItem(todo.id);

  return (
    <>
      {todo.content}
      <button onClick={handleToggleTodoListItemStatus}>{todo.done ? "未完了リストへ" : "完了リストへ"}</button>
      <button onClick={handleDeleteTodoListItem}>削除</button>
    </>
  );
};
```

```js:src\components\TodoList.js
import { TodoTitle } from "./TodoTitle";
import { TodoItem } from "./TodoItem";

// TodoItemをループして表示
// todoListが0件の場合、タイトルとTODOリストを表示しない
export const TodoList = ({ todoList, toggleTodoListItemStatus, deleteTodoListItem, title, as }) => {
  return (
    <>
      {todoList.length !== 0 && (
        <>
          <TodoTitle title={title} as={as} />
          <ul>
            {todoList.map((todo) => (
              <li key={todo.id}>
                <TodoItem todo={todo} key={todo.id} toggleTodoListItemStatus={toggleTodoListItemStatus} deleteTodoListItem={deleteTodoListItem} />
              </li>
            ))}
          </ul>
        </>
      )}
    </>
  );
};
```

```js:src\components\TodoTitle.js
import React, { memo } from "react";

// タイトルの表示コンポーネント
export const TodoTitle = memo(({ title, as }) => {
  if (as === "h1") {
    return <h1>{title}</h1>;
  } else if (as === "h2") {
    return <h2>{title}</h2>;
  } else {
    return <p>{title}</p>;
  }
});
```

```js:src\hooks\useTodo.js
import React, { useState, useEffect } from "react";
import { ulid } from "ulid";

import * as todoData from "../apis/todos";

export const useTodo = () => {
  const [todoList, setTodoList] = useState([]);

  useEffect(() => {
    todoData.getAllTodosData().then((todo) => {
      console.log(todo);
      setTodoList([...todo].reverse());
    });
  }, []);

  // todoのdoneを反転させる
  const toggleTodoListItemStatus = (id, done) => {
    // todoListから、idが一致する1件を取り出す
    const todoItem = todoList.find((item) => item.id === id);
    // doneを反転させて、新たなitemを作成
    const newTodoItem = { ...todoItem, done: !done };
    // サーバに更新API呼ぶ
    todoData.updateTodoData(id, newTodoItem).then((updatedTodo) => {
      // 成功したら、todoListを更新。idが一致しているものを、サーバーから返ってきたupdatedTodoで更新する
      const newTodoList = todoList.map((item) => (item.id !== updatedTodo.id ? item : updatedTodo));
      // 新しいtodoListをstateにセットする
      setTodoList(newTodoList);
    });
  };

  const addTodoListItem = (todoContent) => {
    // あたらしいitemを作成する
    const newTodoItem = { id: ulid, content: todoContent, done: false };
    // サーバーの追加APIを呼ぶ
    todoData.addTodoData(newTodoItem).then((addTodo) => {
      // addTodoをtodoListに追加してstateにセットする
      setTodoList([addTodo, ...todoList]);
    });
  };

  const deleteTodoListItem = (id) => {
    // サーバーの削除APIを呼ぶ
    todoData.deleteTodoData(id).then((deletedid) => {
      const newTodoList = todoList.filter((item) => item.id !== deletedid);
      // 1件削除された新しいtodoListに追加してstateにセットする
      setTodoList(newTodoList);
    });
  };

  // 作成した関数を返す
  return { todoList, toggleTodoListItemStatus, addTodoListItem, deleteTodoListItem };
};
```