React＋TypeScript＋axiosでTODOアプリ作成（Docker環境）

# はじめに
ReactとTypeScriptでTODOアプリを作成しました。
その作成工程を記します。
（工程の最後にtailwindcssをあてましたが、その部分は別記事となります。）
# 環境構築と前準備
以下の記事を参考に環境構築を行って下さい。
[**Docker + React + TypeScript の 環境構築方法**](https://qiita.com/91works-i-kato/items/9f9ad03fee32d42ed547)

https://qiita.com/91works-i-kato/items/9f9ad03fee32d42ed547

バックエンド（API）は以下の記事を参考に作成し、起動しておいてください。
[**DockerとLaravel10でAPI作成（Postmanで動作確認）**](https://qiita.com/91works-i-kato/items/73ff6e70950c93d79b61)

https://qiita.com/91works-i-kato/items/73ff6e70950c93d79b61

API通信用にaxiosを使えるようにします。

:::note info
axiosとは非同期通信でデータを取得するためのモジュールです。
Node.jsで書かれています。 読み方は、アクシオスと読みます。
:::
axiosインストールコマンド
```
npm install axios
```
# srcフォルダ下に機能毎の新たなフォルダを作成
**types (型定義用)**
**apis (APIとの通信用)**
**hooks (hook用)**
**components (システム構成要素用)**
# 機能毎にソースファイルを作成
### typesフォルダ
typesフォルダ内にTodo.tsファイルを作成し、以下を入力
<details><summary>Todo.tsソースコード</summary>

```Todo.ts
export type Todo = {
  id: string;
  text: string;
  updated_at: string;
};
```
</details>

### apisフォルダ
apisフォルダ内にtodos.tsファイルを作成し、以下を入力
<details><summary>todos.tsソースコード</summary>

```todos.ts
import axios from "axios";

const todoDataUrl = "http://localhost/api/todo"

export const getAllTodosData = async () => {
    const response = await axios.get(todoDataUrl);
    return response.data;
};
export const addTodoData = async (text: string) => {
  const response = await axios.post(`${todoDataUrl}`,{text:text});
  return response.data;
};
export const deleteTodoData = async (id: string) => {
  await axios.delete(`${todoDataUrl}/?id=${id}`);
  return id;
};
export const updateTodoData = async (id: string, text: string | undefined) => {
  const response = await axios.put(`${todoDataUrl}/?id=${id}`, {text:text});
  return response.data;
};
```
</details>

### hooksフォルダ
hooksフォルダ内にuseTodo.tsファイルを作成し、以下を入力
<details><summary>useTodo.tsソースコード</summary>

```useTodo.ts
import { useState, useEffect } from "react";
import * as todoData from "../apis/todos";
import { Todo } from "../types/Todo";

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

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

  const updateTodoListItem = (id: string, value: string) => {
    const todoItem = todoList.find((item: Todo) => item.id === id);
    if(!todoItem){
        return;
    }
    const newTodoItem: string = value;
    
    todoData.updateTodoData(id, newTodoItem).then((updatedTodo) => {
      const newTodoList = todoList.map((item) => (item.id !== updatedTodo.id ? item : updatedTodo));
      setTodoList(newTodoList);
    });
  };

  const addTodoListItem = (todoText: string) => {
    const newTodoItem = todoText;

    todoData.addTodoData(newTodoItem).then((addTodo) => {
      setTodoList([addTodo, ...todoList]);
    });
  };

  const deleteTodoListItem = (id: string) => {
    todoData.deleteTodoData(id).then((deletedid) => {
      const newTodoList = todoList.filter((item) => item.id !== deletedid);
      setTodoList(newTodoList);
    });
  };

  return { todoList, updateTodoListItem, addTodoListItem, deleteTodoListItem };
};
```
</details>

### componentsフォルダ
componentsフォルダ内に各ファイルを作成し、
TodoTitle.tsx
TodoAdd.tsx
Todoitem.tsx
TodoList.tsx
App.tsx
以下を入力
<details><summary>TodoTitle.tsxソースコード</summary>

```TodoTitle.tsx
import { 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>;
  }
});
```
</details>

<details><summary>TodoAdd.tsxソースコード</summary>

```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>
    </>
  );
};
```
</details>

<details><summary>Todoitem.tsxソースコード</summary>

```Todoitem.tsx
import React,{useState} from "react";
import { Todo } from "../types/Todo";

export const TodoItem = ({ todo, updateTodoListItem, deleteTodoListItem }: { todo: Todo; updateTodoListItem: any; deleteTodoListItem: any }) => {
  
  const [value, setValue] = useState("");
  const [disabled, setDisabled] = useState(true);
  const [active, setActive] = useState(false);

  const handleChangeText = (event: { target: { value: React.SetStateAction<string>; }; }) => {
    if(event.target.value===""){
      setDisabled(true);
      setActive(false);  
    }else{
      setDisabled(false);
      setActive(true);
      setValue(event.target.value);
    }
  };
  
  const handleUpdateTodoListItem = () =>{
    updateTodoListItem(todo.id,value);
    setDisabled(true);
    setActive(false);
  }

  const handleDeleteTodoListItem = () => deleteTodoListItem(todo.id);

  const year = todo.updated_at.substring(0,4);
  const month = todo.updated_at.substring(5,7);
  const day = todo.updated_at.substring(8,10);
  const hour = String(Number(todo.updated_at.substring(11,13)) + 9);
  const minute = todo.updated_at.substring(14,16);
  const second = todo.updated_at.substring(17,19);
  const datetime = year+"年"+month+"月"+day+"日"+hour+"時"+minute+"分"+second+"秒"

  return (
    <>
      <td>{todo.id}</td>
      <td><input type="text" defaultValue={todo.text} onChange={handleChangeText} /></td>
      <td><input type="text" value={datetime} /></td>
      <td><button id={todo.id} onClick={handleUpdateTodoListItem} disabled={disabled}>{!active ? "":"更新"}</button></td>
      <td><button type="button" onClick={handleDeleteTodoListItem}>削除</button></td>
    </>
  );
};
```
</details>

<details><summary>TodoList.tsxソースコード</summary>

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

export const TodoList = ({
  todoList,
  updateTodoListItem,
  deleteTodoListItem,
  title,
  as,
}: {
  todoList: Todo[];
  updateTodoListItem: (id: string,value: string) => void;
  deleteTodoListItem: (id: string) => void;
  title: string;
  as: string;
}) => {
  return (
    <>
      {todoList.length !== 0 && (
        <>
        <TodoTitle title={title} as={as} />
        <div>
          <table>
            <tr>
              <th>ＮＯ</th><th>　　　　　ＴＯＤＯ　内容　　　　　</th><th>ＴＯＤＯ追加更新日時</th><th>　更新　</th><th>　削除　</th>
            </tr>
            {todoList.map((todo) => (
              <tr key={todo.id}>
                <TodoItem todo={todo} key={todo.id} updateTodoListItem={updateTodoListItem} deleteTodoListItem={deleteTodoListItem} />
              </tr>
            ))}
          </table>
        </div>
        </>
      )}
    </>
  );
};
```
</details>

<details><summary>App.tsxソースコード</summary>

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

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

function App() {
  const { todoList, updateTodoListItem, 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.text);

  return (
    <>
  <TodoTitle title="TODOアプリ" as="h1" />
  <TodoAdd buttonText="追加" inputEl={inputEl} handleAddTodoListItem={handleAddTodoListItem} />
  <TodoList todoList={incompletedList} updateTodoListItem={updateTodoListItem} deleteTodoListItem={deleteTodoListItem} title="TODOリスト" as="h2" />
  </>
  );
}

export default App;
```
</details>

### srcフォルダ
:::note alert
srcフォルダ内のApp.tsxファイルは削除しておいてください。
:::
srcフォルダ内にあるindex.tsxファイルの内容を以下に変更
<details><summary>index.tsxソースコード</summary>

```index.tsx
import React from "react";
import ReactDOM from "react-dom";
import './index.css';
import App from "./components/App";

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

# プログラムの実行
API（バックエンド側）が起動している状態で、下記コマンドを実行します。
```
docker-compose build
```
実行後
```
docker-compose up –d
```
を実行

コンテナが起動している状態になりますので、Webブラウザを開いて「 http://localhost:8000/ 」にアクセスします。

以上になります。

# 最後に
今回、実行後の画面画像等については、記事にしておりません。
今後、TailWindCssおよびCSSで加工した画面と作成工程を、別記事にて公開する予定です。
画面デザインに関しましては、そちらの記事を参考にして頂くか、
もしくは、ご自身でCSSをあてて、画面の見栄えを良くして頂けましたら幸いです。

最後まで読んでいただきまして、ありがとうございました。