1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-09-20

6-2 TODOアプリの実装

参考
基礎から学ぶReact/React Hooks

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

  • json-serverの起動
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
    }
  ]
}
json-server --watch db.json --port 3100

TODOアプリの作成(リスト取得のみ)

image.png

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

  • todoListをdoneのture/falseによって、incompletedListとcompletedListに分ける
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をループして表示
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の作成

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を取得、追加、更新、削除するカスタムフックを作成する

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

src/
┣ apis/
┃ ┗ todos.js
┣ components/
┃ ┣ App.js
┃ ┣ TodoAdd.js
┃ ┣ TodoItem.js
┃ ┣ TodoList.js
┃ ┗ TodoTitle.js
┣ hooks/
┃ ┗ useTodo.js
┗ index.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;
};
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;
src\components\TodoAdd.js
export const TodoAdd = ({ buttonText, inputEl, handleAddTodoListItem }) => {
  return (
    <>
      <textarea ref={inputEl} />
      <button onClick={handleAddTodoListItem}>{buttonText}</button>
    </>
  );
};
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>
    </>
  );
};
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>
        </>
      )}
    </>
  );
};
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>;
  }
});
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 };
};
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?