📚

Rails7とReact18を使ってTodoアプリを作ります

2023/07/10に公開

記事を書くきっかけ

RailsとReactを使ってWebアプリケーションの基本的な機能となるCRUD処理の実装方法について改めて学び直したので記事にしました。

ゴール

RailsとReactを使ってTodoアプリを作成する。

準備

私が以前に作成したこちらの記事をもとに環境構築をしたところから始めます。

バックエンド

モデル

Todoモデルを作成します。

terminal
docker compose exec api rails g model Todo

作成されたマイグレーションファイルを次のように編集します。
今回作成するTodoアプリではタイトルと完了未完了を管理するカラムを追加しています。
完了未完了を判断するカラムはデフォルトではfalseとしました。

backend/db/migrate/20230708064115_create_todos.rb
class CreateTodos < ActiveRecord::Migration[7.0]
  def change
    create_table :todos do |t|
      t.string :title
      t.boolean :completed, default: false

      t.timestamps
    end
  end
end

rails db:migrateを実行します。

terminal
docker compose exec api rails db:migrate

バリデーションを設定します。
タイトルを必須にしました。

backend/app/models/todo.rb
class Todo < ApplicationRecord
  validates :title, presence: true
end

コントローラ

コントローラを作成します。
apiはバージョンで管理することが多いと思いますのでapi/v1/配下にコントローラを作成しました。

terminal
docker compose exec api rails g controller api/v1/todos

before_actionでset_todoを定義しています。updateとdestroyのアクションの前で対象のTodoを取得しています。updateでは完了未完了を更新します。

backend/app/controllers/api/v1/todos_controller.rb
class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: [:update, :destroy]

  def index
    @todos = Todo.all
    render json: @todos
  end

  def create
    @todo = Todo.new(todo_params)

    if @todo.save
      render json: @todo, status: :created, location: @todo
    else
      render json: @todo.errors, status: :unprocessable_entity
    end
  end

  def update
    if @todo.update(todo_params)
      render json: @todo
    else
      render json: @todo.errors, status: :unprocessable_entity
    end
  end

  def destroy
    @todo.destroy
    head :no_content
  end

  private

  def set_todo
    @todo = Todo.find(params[:id])
  end

  def todo_params
    params.require(:todo).permit(:title, :completed)
  end
end

ルーティング

ルーティングを設定します。

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
     resources :todos, only: [:index, :create, :update, :destroy]
    end 
  end 
end

以上でバックエンドは完了です。

フロントエンド

ライブラリのインストール

必要なライブラリをインストールします。
今回はaxiosとChakra UIとChakra UI Iconをインストールします。
axiosを使ってバックエンドと通信を行います。
Chakra UIとそのアイコンを使ってUIを作成します。

terminal
docker compose exec front npm install axios @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/icons

APIを叩く

APIに対して操作を行うためのファイルを作成します。

frontend/app/src/lib/api/client.ts
import axios from "axios";

const client = axios.create({
  baseURL: "http://localhost:3001/api/v1",
});

export default client;
frontend/app/src/lib/api/todos.ts
import { TodoType } from "../../types/todo";
import client from "./client";

export const getTodos = () => {
  return client.get<TodoType[]>("/todos");
};

export const createTodo = (todo: Pick<TodoType, "title" | "completed">) => {
  return client.post("/todos", todo);
};

export const updateTodo = (id: number, todo: Pick<TodoType, "completed">) => {
  return client.put(`/todos/${id}`, todo);
};

export const deleteTodo = (id: number) => {
  return client.delete(`/todos/${id}`);
};

型定義

Todoの型を定義します。

frontend/app/src/types/todo.ts
export type TodoType = {
  id: number;
  title: string;
  completed: boolean;
};

見た目を作成

App.tsxを編集します。

frontend/app/src/App.tsx
import React, { useEffect, useState } from "react";
import { TodoType } from "./types/todo";
import { createTodo, deleteTodo, getTodos, updateTodo } from "./lib/api/todos";
import { AddIcon, DeleteIcon } from "@chakra-ui/icons";
import {
  Box,
  Button,
  Checkbox,
  Flex,
  Heading,
  Input,
  VStack,
} from "@chakra-ui/react";

const App = () => {
  const [todos, setTodos] = useState<TodoType[]>([]);
  const [title, setTitle] = useState<string>("");

  const handleCreateTodo = async () => {
    const response = await createTodo({
      title: title,
      completed: false,
    });
    setTodos([...todos, response.data]);
    setTitle("");
  };

  const handleToggleTodo = async (id: number, completed: boolean) => {
    const response = await updateTodo(id, {
      completed: !completed,
    });
    setTodos(todos.map((todo) => (todo.id === id ? response.data : todo)));
  };

  const handleDeleteTodo = async (id: number) => {
    await deleteTodo(id);
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  useEffect(() => {
    getTodos().then((response) => setTodos(response.data));
  }, []);

  return (
    <VStack spacing={4} padding={4}>
      <Heading as="h1" mb="8">
        Todoアプリ
      </Heading>
      <Flex>
        <Input
          placeholder="Todoを入力"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <Button colorScheme="blue" mx={4}>
          <AddIcon onClick={handleCreateTodo} />
        </Button>
      </Flex>
      {todos.map((todo) => (
        <Box
          key={todo.id}
          display="flex"
          flexDirection="row"
          alignItems="center"
        >
          <Checkbox
            isChecked={todo.completed}
            onChange={() => handleToggleTodo(todo.id, todo.completed)}
          >
            {todo.title}
          </Checkbox>
          <DeleteIcon onClick={() => handleDeleteTodo(todo.id)} />
        </Box>
      ))}
    </VStack>
  );
};

export default App;

結果

つぎのような仕上がりとなった。

まとめ

RailsとReact×TypeScriptを利用してシンプルなTodoアプリを作成することができました。
よりよいコードの書き方があると思いますので今後も学習を続けたいと思います!

Discussion