Rails7とReact18を使ってTodoアプリを作ります
記事を書くきっかけ
RailsとReactを使ってWebアプリケーションの基本的な機能となるCRUD処理の実装方法について改めて学び直したので記事にしました。
ゴール
RailsとReactを使ってTodoアプリを作成する。
準備
私が以前に作成したこちらの記事をもとに環境構築をしたところから始めます。
バックエンド
モデル
Todoモデルを作成します。
docker compose exec api rails g model Todo
作成されたマイグレーションファイルを次のように編集します。
今回作成するTodoアプリではタイトルと完了未完了を管理するカラムを追加しています。
完了未完了を判断するカラムはデフォルトではfalseとしました。
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を実行します。
docker compose exec api rails db:migrate
バリデーションを設定します。
タイトルを必須にしました。
class Todo < ApplicationRecord
validates :title, presence: true
end
コントローラ
コントローラを作成します。
apiはバージョンで管理することが多いと思いますのでapi/v1/配下にコントローラを作成しました。
docker compose exec api rails g controller api/v1/todos
before_actionでset_todoを定義しています。updateとdestroyのアクションの前で対象のTodoを取得しています。updateでは完了未完了を更新します。
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を作成します。
docker compose exec front npm install axios @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/icons
APIを叩く
APIに対して操作を行うためのファイルを作成します。
import axios from "axios";
const client = axios.create({
baseURL: "http://localhost:3001/api/v1",
});
export default client;
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の型を定義します。
export type TodoType = {
id: number;
title: string;
completed: boolean;
};
見た目を作成
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