React + TypeScript で Todoアプリの作成

# ToDoアプリの仕様
- 完了／未完了別にリスト化して表示(Read)
- 追加ができる(Create)
- 状態（完了／未完了）をボタンクリックで変更できる(Update)
- 削除ができる(Delete)
- モックサーバーでTODO情報を更新管理できる

# Create React Appの実行

```sh
# nodeのバージョン確認
node -v
# create-react-app プロジェクト名 --template typescript でアプリ作成
npx create-react-app todo-app --template typescript
# 作成されたディレクトリに移動
cd todo-app
# アプリ起動
npm start
```

# 不要ファイルの削除と修正

```
todo-app/
┣ node_modules/
┣ public/
┣ src/
┃ ┣ App.css  ← 削除
┃ ┣ App.tsx
┃ ┣ App.test.tsx  ← 削除
┃ ┣ index.css  ← 削除
┃ ┣ index.tsx
┃ ┣ logo.svg  ← 削除
┃ ┣ react-app-env.d.ts
┃ ┣ reportWebVitals.ts  ← 削除
┃ ┗ setupTests.ts  ← 削除
┣ .gitignore
┣ package.json
┣ package-lock.json
┣ README.md
┗ tsconfig.lock
```


```tsx:App.tsx
function App() {
  return <p>これからTODOアプリを実装します！</p>;
}

export default App;
```


```tsx:index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
```
- ブラウザで、`http://localhost:3000`を開く
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/29202f22-39b7-8191-5bf5-f8424c9eb014.png)



## モックサーバーの準備

### データベースファイル、`db.json`をプロジェクト直下に作成する。

```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
    }
  ]
}
```

### 起動方法（ポート3100番で起動）

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

- ブラウザで、`http://localhost:3100/todos`を開くと一覧が取得できる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/91656837-096b-3934-dd01-1ec3fa100e20.png)

- axiosとulidのインストール
    - ユニークなIDを付与するために、ulidをインストールする。ソート可能でランダムなIDを生成する。

```sh
npm install axios
npm install ulid
```

# 完成形

```
src/
┣ apis/
┃ ┗ todos.ts
┣ components/
┃ ┣ App.tsx
┃ ┣ TodoAdd.tsx
┃ ┣ TodoItem.tsx
┃ ┣ TodoList.tsx
┃ ┗ TodoTitle.tsx
┣ hooks/
┃ ┗ useTodo.ts
┣ types/
┃ ┗ Todo.ts
┣ index.tsx
┗ react-app-env.d.ts
```

### apis/todos.ts
- サーバAPIとのアクセス部分

```tsx:apis/todos.ts
import axios from "axios";
import { Todo } from "../types/Todo";

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: Todo) => {
  const response = await axios.post(todoDataUrl, todo);
  return response.data;
};

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

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

```
### components/App.tsx
- メイン部分

```tsx:components/App.tsx
import React, { useRef } from "react";

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

function App() {
  // カスタムフックから必要な変数を取得
  const { todoList, toggleTodoListItemStatus, addTodoListItem, deleteTodoListItem } = useTodo();

  const inputEl = useRef<HTMLTextAreaElement>(null);

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

  // 未完了リスト
  const incompletedList = todoList.filter((todo: Todo) => !todo.done);
  // 完了リスト
  const completedList = todoList.filter((todo: 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;

```

### components/TodoAdd.tsx
- 「Todoを追加」するコンポーネント

```tsx:components/TodoAdd.tsx
import { RefObject } from "react";

export const TodoAdd = ({ buttonText, inputEl, handleAddTodoListItem }: { buttonText: string; inputEl: RefObject<HTMLTextAreaElement>; handleAddTodoListItem: () => void }) => {
  return (
    <>
      <textarea ref={inputEl} />
      <button onClick={handleAddTodoListItem}>{buttonText}</button>
    </>
  );
};

```

### components/TodoItem.tsx
- Todoを表示するコンポーネント

```tsx:components/TodoItem.tsx
import { Todo } from "../types/Todo";

// 1つのTodo、内容と移動・削除ボタン
export const TodoItem = ({ todo, toggleTodoListItemStatus, deleteTodoListItem }: { todo: Todo; toggleTodoListItemStatus: any; deleteTodoListItem: any }) => {
  // 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>
    </>
  );
};

```

### components/TodoList.tsx
- Todoを一覧表示するコンポーネント

```tsx:components/TodoList.tsx
import { TodoTitle } from "./TodoTitle";
import { TodoItem } from "./TodoItem";
import { Todo } from "../types/Todo";

// TodoItemをループして表示
// todoListが0件の場合、タイトルとTODOリストを表示しない
export const TodoList = ({
  todoList,
  toggleTodoListItemStatus,
  deleteTodoListItem,
  title,
  as,
}: {
  todoList: Todo[];
  toggleTodoListItemStatus: (id: string, status: boolean) => void;
  deleteTodoListItem: (id: string) => void;
  title: string;
  as: string;
}) => {
  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>
        </>
      )}
    </>
  );
};

```

### components/TodoTitle.tsx
- タイトルを表示するコンポーネント

```tsx:components/TodoTitle.tsx
import React, { memo } from "react";

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

### components/useTodo.tsx
- Todoの追加、更新、削除、一覧の取得を行うカスタムフック

```tsx:hooks/useTodo.ts
import React, { useState, useEffect } from "react";
import { ulid } from "ulid";

import * as todoData from "../apis/todos";
import { Todo } from "../types/Todo";

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

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

  // todoのdoneを反転させる
  const toggleTodoListItemStatus = (id: string, done: boolean) => {
    // todoListから、idが一致する1件を取り出す
    const todoItem = todoList.find((item: Todo) => item.id === id);
    // doneを反転させて、新たなitemを作成
    const newTodoItem: Todo = { ...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: string) => {
    // あたらしいitemを作成する
    const newTodoItem = { id: ulid(), content: todoContent, done: false };

    // サーバーの追加APIを呼ぶ
    todoData.addTodoData(newTodoItem).then((addTodo) => {
      // addTodoをtodoListに追加してstateにセットする
      setTodoList([addTodo, ...todoList]);
    });
  };

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

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

```

### types/Todo.ts
- Todoの型

```tsx:types/Todo.ts
export type Todo = {
  id: string;
  content: string;
  done: boolean;
};

```

### index.tsx

Appのimportの部分を修正

```react
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
```


### 完成した画面イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/639c119d-81b2-fe03-3ff6-3ec985bc795e.png)


### 参考
- [基礎から学ぶReact/React Hooks](https://www.c-r.com/book/detail/1417)