前回は ToDo アプリケーションのバックエンドの実装を行いました. 今回は前回作成した API を用いて ToDo アプリケーションのフロントエンドを実装していきたいと思います.

準備

HTTP クライアントとして Axios を使用します. ./fontディレクトリに移動し, npm を用いてインストールします.

npm install axios

実装に取り掛かる前に余分なファイルを削除します.

.
├── index.html
├── node_modules [61 entries exceeds filelimit, not opening dir]
├── package-lock.json
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.tsx
│   ├── main.tsx
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

./front/src/App.tsxの中身を削除し, 以下のように書き換えます.

import React from "react";

const App = () => {
  return <div>App</div>;
};

export default App;

ToDo 一覧の取得

まずは ToDo 一覧を取得する処理を実装します. ./front/src/App.tsxの中身を以下のように書き換えます.

import React from "react";
import axios from "axios";

const baseURL = "http://localhost:8000/todos";

type Todo = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
};

const App = () => {
  const [todos, setTodos] = React.useState<Todo[]>([]);

  const getTodos = async () => {
    const response = await axios.get(baseURL);
    setTodos(response.data);
  };

  React.useEffect(() => {
    getTodos();
  }, []);

  return (
    <div>
      <h1>Todo List</h1>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input type="checkbox" checked={todo.completed} onChange={() => putTodo(todo)} />
            <span style={{ paddingRight: "2rem" }}>{todo.title}</span>
            <span style={{ paddingRight: "2rem" }}>{todo.description}</span>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

無事に ToDo 一覧が表示されたら成功です.

image

ToDo の追加

フォームを用いて ToDo を追加できるようにします. フォームの内容を State で保持し, Submit ボタンが押されたら post リクエストを送信します. ./front/src/App.tsxの中身を以下のように書き換えます.

import React from "react";
import axios from "axios";

const baseURL = "http://localhost:8000/todos";

type Todo = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
};

type PostBody = {
  title: string;
  description: string;
  completed: boolean;
};

const App = () => {
  const [todos, setTodos] = React.useState<Todo[]>([]);
  const [postBody, setPostBody] = React.useState<PostBody>({
    title: "",
    description: "",
    completed: false,
  });

  const getTodos = async () => {
    const response = await axios.get(baseURL);
    setTodos(response.data);
  };

  const postTodo = async () => {
    await axios.post(baseURL, postBody).then((response) => {
      setTodos([...todos, response.data]);
    });
  };

  const handleChanges = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPostBody({ ...postBody, [e.target.name]: e.target.value });
  };

  React.useEffect(() => {
    getTodos();
  }, []);

  return (
    <div>
      <h1>Todo List</h1>
      <input name="title" value={postBody.title} onChange={handleChanges} /> <br />
      <input name="description" value={postBody.description} onChange={handleChanges} /> <br />
      <button onClick={postTodo}>Submit</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span style={{ paddingRight: "2rem" }}>{todo.title}</span>
            <span style={{ paddingRight: "2rem" }}>{todo.description}</span>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

フォームに ToDo のタイトルと説明を入力し, Submit ボタンを押すと, ToDo が追加されます.

image

ToDo の更新

ToDo のチェックボックスをクリックすると, その ToDo の completed を更新します. ./front/src/App.tsxの中身を以下のように書き換えます.

import React from "react";
import axios from "axios";

const baseURL = "http://localhost:8000/todos";

type Todo = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
};

type PostBody = {
  title: string;
  description: string;
  completed: boolean;
};

const App = () => {
  const [todos, setTodos] = React.useState<Todo[]>([]);
  const [postBody, setPostBody] = React.useState<PostBody>({
    title: "",
    description: "",
    completed: false,
  });

  const getTodos = async () => {
    const response = await axios.get(baseURL);
    setTodos(response.data);
  };

  const postTodo = async () => {
    await axios.post(baseURL, postBody).then((response) => {
      setTodos([...todos, response.data]);
    });
  };

  const putTodo = async (todo: Todo) => {
    await axios
      .put(`${baseURL}/${todo.id}`, {
        ...todo,
        completed: !todo.completed,
      })
      .then((response) => {
        setTodos(todos.map((todo) => (todo.id === response.data.id ? response.data : todo)));
      });
  };

  const handleChanges = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPostBody({ ...postBody, [e.target.name]: e.target.value });
  };

  React.useEffect(() => {
    getTodos();
  }, []);

  return (
    <div>
      <h1>Todo List</h1>
      <input name="title" value={postBody.title} onChange={handleChanges} /> <br />
      <input name="description" value={postBody.description} onChange={handleChanges} /> <br />
      <button onClick={postTodo}>Submit</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input type="checkbox" checked={todo.completed} onChange={() => putTodo(todo)} />
            <span style={{ paddingRight: "2rem" }}>{todo.title}</span>
            <span style={{ paddingRight: "2rem" }}>{todo.description}</span>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

チェックボックスを押した後, ページをリロードしてもチェックボックスの状態が保持されていることを確認してください.

image

ToDo の削除

Delete ボタンが押されたら Delete リクエストを送信し, その ToDo を削除します. ./front/src/App.tsxの中身を以下のように書き換えます.

import React from "react";
import axios from "axios";

const baseURL = "http://localhost:8000/todos";

type Todo = {
  id: string;
  title: string;
  description: string;
  completed: boolean;
};

type PostBody = {
  title: string;
  description: string;
  completed: boolean;
};

const App = () => {
  const [todos, setTodos] = React.useState<Todo[]>([]);
  const [postBody, setPostBody] = React.useState<PostBody>({
    title: "",
    description: "",
    completed: false,
  });

  const getTodos = async () => {
    const response = await axios.get(baseURL);
    setTodos(response.data);
  };

  const postTodo = async () => {
    await axios.post(baseURL, postBody).then((response) => {
      setTodos([...todos, response.data]);
    });
  };

  const putTodo = async (todo: Todo) => {
    await axios
      .put(`${baseURL}/${todo.id}`, {
        ...todo,
        completed: !todo.completed,
      })
      .then((response) => {
        setTodos(todos.map((todo) => (todo.id === response.data.id ? response.data : todo)));
      });
  };

  const deleteTodo = async (todo: Todo) => {
    await axios.delete(`${baseURL}/${todo.id}`).then((response) => {
      setTodos(todos.filter((todo) => todo.id !== response.data.id));
    });
  };

  const handleChanges = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPostBody({ ...postBody, [e.target.name]: e.target.value });
  };

  React.useEffect(() => {
    getTodos();
  }, []);

  return (
    <div>
      <h1>Todo List</h1>
      <input name="title" value={postBody.title} onChange={handleChanges} /> <br />
      <input name="description" value={postBody.description} onChange={handleChanges} /> <br />
      <button onClick={postTodo}>Submit</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input type="checkbox" checked={todo.completed} onChange={() => putTodo(todo)} />
            <span style={{ paddingRight: "2rem" }}>{todo.title}</span>
            <span style={{ paddingRight: "2rem" }}>{todo.description}</span>
            <button onClick={() => deleteTodo(todo)}>delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default App;

Delete ボタンを押した後, ページをリロードしても削除された ToDo が表示されないことを確認してください.

image

終わりに

フロントエンドの実装は以上です. こちらは駆け足となってしまいましたが, 無事 FARM 構成のフルスタックアプリケーションを作成することができました.