LoginSignup
26
10

More than 1 year has passed since last update.

React + TypeScript で Todoアプリの作成

Last updated at Posted at 2022-03-17

ToDoアプリの仕様

  • 完了/未完了別にリスト化して表示(Read)
  • 追加ができる(Create)
  • 状態(完了/未完了)をボタンクリックで変更できる(Update)
  • 削除ができる(Delete)
  • モックサーバーでTODO情報を更新管理できる

Create React Appの実行

# 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
App.tsx
function App() {
  return <p>これからTODOアプリを実装します!</p>;
}

export default App;
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

モックサーバーの準備

データベースファイル、db.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番で起動)

npx json-server --watch db.json --port 3100
  • ブラウザで、http://localhost:3100/todosを開くと一覧が取得できる

image.png

  • axiosとulidのインストール
    • ユニークなIDを付与するために、ulidをインストールする。ソート可能でランダムなIDを生成する。
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とのアクセス部分
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

  • メイン部分
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を追加」するコンポーネント
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を表示するコンポーネント
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を一覧表示するコンポーネント
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

  • タイトルを表示するコンポーネント
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の追加、更新、削除、一覧の取得を行うカスタムフック
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の型
types/Todo.ts
export type Todo = {
  id: string;
  content: string;
  done: boolean;
};

index.tsx

Appのimportの部分を修正

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

参考

26
10
2

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
26
10