【React ✖️ Ruby on Rails】Todoリストを進化させてみた

## 前回のはなし:page_facing_up:

前回の初投稿でReactを使ってTodoリストを作成を作成しました。

記事はこちら↓

https://qiita.com/kurumi-program/items/0e5412cdc59643683408

でもこれだと…

**リロードすると元に戻ってしまう → 値が保存されていない！！！**

そうなるとこのTodoリストを使った人の末路は…

自分「よし、今日のやること書いたぞ！これで頑張ろ！」
<font color="Green">...1時間後</font>
自分「あーー、ブラウザのタブ開きすぎてもうよく分からなくなってきた…一旦全部閉じよ」
<font color="Green">再びブラウザ開き、作業に入る</font>
自分「そういえば、これ終わったら何するんだっけ？Todoリスト確認するか！」
<font color="Green">確認しようとTodoリストを起動</font>
自分「あれ？何も書いていないじゃん！
ってことは、<font color="#E0556A">今日のやることはもう終わったのか！！</font>よっしゃーー！アニメでも観よ！」

ってなります。
なので今回は、登録したタスクのデータを保存していこうと思います！
それではレッツゴー〜⭐️

## 【Rails編 ①】 プロジェクトの作成

```zsh:ターミナル
rails new todo_api --api
```
プロジェクトが作成されたら、プロジェクトに移動して(todo_api直下で)、一旦起動できるか確認してみます。

```zsh:ターミナル(todo_api)
rails s
```
http://localhost:3000 にアクセスして以下の画面が出たら、成功です！

![スクリーンショット 2024-10-08 21.21.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3886720/2831180f-8cfe-19ac-57c1-325665050053.png)

因みに上の通り、Railsのバージョンは7.1.4、Rubyのバージョンは3.2.2

## 【Rails編 ②】 ポート設定

一旦、起動したサーバーを切って(control + c)、下記のようにポート番号を書き換えます。

```diff_ruby:config/puma.rb
- port ENV.fetch("PORT") { 3000 }
+ port ENV.fetch("PORT") { 3010 }
```

reactでポート3000番を使用するのでそれ以外の番号に書き換えます。
今回は3010番にしています。

## 【Rails編 ③】 CORS の設定

何も設定しないと、Reactにアクセスすることができないのでここで設定をしていきます。なのでCORSの設定ができるgemライブラリを使用します↓

```Gemfile:Gemfile
gem "rack-cors"
```
rack-corsという記述が書いてある部分のコメントアウトを外します。

```zsh:ターミナル(todo_api)
bundle install
```
インストールできたら、設定を記述していきます↓

```diff_ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
-    origins "example.com"
+    origins "http://localhost:3000"

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

**origins "http://localhost:3000"**
http://localhost:3000 からの通信を許可しています。

**resource "*"**
"*"によりすべてのリソースへのアクセスを許可しています。

**headers: :any**
リクエストで送信可能なすべてのヘッダーを許可しています。

**methods:~**
許可するHTTPメソッドを指定しています。

※因みにバージョン等が異なり、ファイルが存在しない場合は新たに作成してください！

## 【Rails編 ④】MVCとルーティング

### 1. Todoモデルの作成

```zsh:ターミナル(todo_api)
rails g model Todo
```
Todoモデルを作り、

```diff_ruby:db/migrate/作った日付_create_todos.rb
class CreateTodos < ActiveRecord::Migration[7.1]
  def change
    create_table :todos do |t|
+     t.string :name
+     t.boolean :completed

      t.timestamps
    end
  end
end
```
:name(Todoリストで追加していくタスク)という 文字列型（string） のカラムと:completed(タスクの状態選択、チェックボックスの部分) というブール型（boolean） のカラムを定義します。
↑後のReactで使います。

```zsh:ターミナル(todo_api)
rails db:migrate
```
このコマンドでテーブルが作成されます。

### 2. コントローラーの作成

コントローラーディレクトリの中にtodos_controller.rbを作成

```diff_ruby:app/controllers/todos_controller.rb
class TodosController < ApplicationController
  def index
    todos = Todo.all

    render json: todos
  end

  def create
    todo = Todo.new(todo_params)
    if todo.save
      render json: todo, status: :created
    else
      render json: todo.errors, status: :unprocessable_entity
    end
  end

  def update
    todo = Todo.find(params[:id])
    if todo.update(todo_params)
      render json: todo
    else
      render json: todo.errors, status: :unprocessable_entity
    end
  end

  def destroy
    todo = Todo.find(params[:id])
    todo.destroy

    render json: todo
  end

  private
  def todo_params
    params.require(:todo).permit(:name, :completed)
  end
end
```
**render json:**
はJSON形式に変換してクライアントに返しているということ。

**def index
~
endの部分**
Todo.all で、todosテーブルの登録した全データを取得します。


**def create
〜
endの部分**
Todo.new(todo_params) で新しい Todo オブジェクトを生成します。
todo.save が成功すれば、HTTPステータスコード 201 Created を返します。
保存に失敗した場合（例: バリデーションエラー）、HTTPステータスコード 422 Unprocessable Entity を返します。
<font color="red">status: :unprocessable_entityがなかったら？</font>
<font color="red">※バリデーションエラーが来ても200 成功したコードが返される可能性があります。<font color="red">

**def update
〜
endの部分**
上のcreateアクションと書き方はほぼ同じ。ただ更新するので該当するTodoをfindでデータベースから検索し、それをアップデートします。

**def destroy
〜
endの部分**
todo.destroy で、その Todo をデータベースから削除しています。

**private
〜
endの部分**
<font color="#E0556A">create</font>や<font color="#E0556A">update</font>アクションなどで、リクエストから受け取ったデータを取得するために使用されます。

### 3. ルーティングの設定

```diff_ruby:config/routes.rb
Rails.application.routes.draw do
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
+  resources :todos, only: [:index, :create, :update, :destroy]
end
```
これでルーティングが設定されます。
次からは、React(フロント)側の設定をしていきます。
と、その前にデータがフロント側で取得出来ているか確認するため、データを1つ作成しておきます。

```zsh:ターミナル
rails c

Todo.create(name:'タスク', completed: false)
```

# 【React編 ①】タスクの取得

基本的には前回作成したTodoリストを書き換えていく感じで進めます↓

https://qiita.com/kurumi-program/items/0e5412cdc59643683408

今回はAPIとの通信を行うために直感的に非同期コードを書くことが可能なAxiosというライブラリを使用します。

```zsh:todo-app
npm install axios
```

```diff_javascript:App.jsx
import './App.css';
import TodoList from './components/TodoList';
import {useState,
+ useEffect
} from 'react';
import { v4 as uuidv4 } from 'uuid';
+ import axios from 'axios'

function App() {
  const [todos, setTodos] = useState([])
  
  const [inputValue, setInputValue] = useState('')

+  const fetch = async () => {
+    try {
+      const res = await axios.get('http://localhost:3010/todos')
+      setTodos(res.data)
+    } catch(e) {
+      console.log(e)
+    }
+  }

+  useEffect(() => {
+    fetch()
+  }, [])
  //...省略

export default App;

```

**axios、useEffect**をimportする。

**const fetch = async () => {
〜
  }**
asyncを使って、関数が非同期関数であることを宣言しています。
**try { ... } catch (e) { ... }**
tryは成功した場合の処理を定義でき、catchはエラーが発生した際の処理を定義できます。

**await axios.get('http://localhost:3010/todos')**
【Rails編 ④】の3でcontrollerを設定した際に**index**の部分で定義したTodo.allを取得しています。awaitを使うことで、リクエストが完了してデータが返ってくるまで待機します。
データが返ってきたらsetTodosの値を更新することでサーバーに保存されたデータを表示できます。
**async、awaitについて↓**
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function

**useEffect(() => {
  fetch()
}, [])**
useEffectが実行されるとfetch関数が呼び出されてタスクを表示することができます。
詳しく知りたい方はこちら↓
https://ja.react.dev/reference/react/useEffect

![スクリーンショット 2024-10-10 0.17.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3886720/0ced513f-f5b7-c14d-42ec-c230dbe87a9d.png)

先ほどrails側で追加した「◻️タスク」が表示されていれば、通信は成功です⭐️
もし上手くいかない場合は、debuggerを仕込み、処理を止めてres.dataの値を見てみましょう！

## 【React編 ②】タスクの追加

```diff_javascript:App.jsx
import './App.css';
import TodoList from './components/TodoList';
import { useState, useEffect } from 'react';
- import { v4 as uuidv4 } from 'uuid';
import axios from 'axios'

function App() {
 //...省略
  const handleAddTodo = async () => {
    if (inputValue === '') return
    try {
      const res = await axios.post('http://localhost:3010/todos', { name: inputValue, completed: false })
      setTodos([...todos, res.data])
    } catch (e) {
      alert('todoの追加に失敗しました')
      console.log(e)
    }
    setInputValue('')
  }
```
**if (inputValue === '') return**
inputValue が空文字列の場合、関数を終了します。つまり、空文字を追加できないようにしています。

**await axios.post('http://localhost:3010/todos', { name: inputValue, completed: false })**
axios.post メソッドを使用して、新しいTodoをサーバーに送信しています。
uuidは必要なくなったので消しておきます。

**catch (e) {
      alert('todoの追加に失敗しました')
      console.log(e)
    }**
失敗した際の処理も入れておきます。

## 【React編 ③】タスクの状態選択機能作成

```diff_javascript:App.jsx
   //...省略
   setInputValue('')
  }
  //ここから
  const todoCompleted = async (id) => {
    const todo = todos.find((todo) => todo.id === id)
    const res = await axios.patch(`http://localhost:3010/todos/${id}`, { completed: !todo.completed })
    setTodos(todos.map(todo => todo.id === id ? res.data : todo))
  }
```

**const res = await axios.patch('http://localhost:3010/todos/${id}', { completed: !todo.completed })**
axios.patch を使って、サーバーに対してTodoのステータスを更新するリクエストを送っています。

**setTodos(todos.map(todo => todo.id === id ? res.data : todo))**
map メソッドを使って、todos リスト内のTodoを更新しています。mapは新しい配列を生成するため、todoのコピーを作る必要はありません！
todo.id === id で、対象となるTodo項目を見つけます。
見つかった場合は、サーバーから返された更新後のTodo（res.data）に置き換え、見つからなかった場合は、そのままのTodoを保持します。

因みに **? :** の意味不明な記号は？
JavaScriptにおける三項演算子（Conditional or Ternary Operator）です。
構文↓
**条件式 ? 真の場合の値 : 偽の場合の値**
割と便利なので覚えておくといいかもです。

## 【React編 ④】タスクの削除


```diff_javascript:App.jsx
   //...省略
    setTodos(todos.map(todo => todo.id === id ? res.data : todo))
  }
　//ここから
  const handleClear = async () => {
    const completedTodos = todos.filter((todo) => todo.completed)
    await Promise.all(completedTodos.map(todo => axios.delete(`http://localhost:3010/todos/${todo.id}`)))
    setTodos(todos.filter(todo => !todo.completed))
  }
```
**const completedTodos = todos.filter((todo) => todo.completed)**
チェックのついているtodoを残しています。

**await Promise.all(completedTodos.map(todo => axios.delete(`http://localhost:3010/todos/${todo.id}`)))**
<font color="#E0556A">Promise.all</font>は、複数のPromiseを同時に実行し、全てのPromiseが解決されるのを待つために使います。
<font color="#E0556A">completedTodos.map(todo => axios.delete(http://localhost:3010/todos/${todo.id}`))`</font>では、完了したTodoのIDを使ってそれぞれのTodoを削除するためのDELETEリクエストを生成しています。
<font color="#E0556A">mapメソッド</font>を使うことで、completedTodos の各Todoに対して削除リクエストを生成し、これを配列として返します。
<font color="#E0556A">axios.delete</font>を使って、サーバーに対してTodoのステータスを削除するリクエストを送っています。
await によって、全ての削除リクエストが完了するまで待機しています。

以上の書き換えによってタスクの値をサーバーに保存させることができます。

## 全ソースコード

```react:App.jsx
import './App.css';
import TodoList from './components/TodoList';
import { useState, useEffect } from 'react';
import axios from "axios"

function App() {
  const [todos, setTodos] = useState([])

  const [inputValue, setInputValue] = useState('')

  const fetch = async () => {
    try {
      const res = await axios.get('http://localhost:3010/todos')
      setTodos(res.data)
    } catch(e) {
      console.log(e)
    }
  }

  useEffect(() => {
    fetch()
  }, [])

  const handleInputValue = (e) => {
    setInputValue(e.target.value)
  }
  const handleAddTodo = async () => {
    if (inputValue === '') return
    try {
      const res = await axios.post('http://localhost:3010/todos', { name: inputValue, completed: false })
      setTodos([...todos, res.data])
    } catch (e) {
      alert('todoの追加に失敗しました')
      console.log(e)
    }
    setInputValue('')
  }

  const todoCompleted = async (id) => {
    const todo = todos.find((todo) => todo.id === id)
    const res = await axios.patch(`http://localhost:3010/todos/${id}`, { completed: !todo.completed })
    setTodos(todos.map(todo => todo.id === id ? res.data : todo))
  }

  const handleClear = async () => {
    const completedTodos = todos.filter((todo) => todo.completed)
    await Promise.all(completedTodos.map(todo => axios.delete(`http://localhost:3010/todos/${todo.id}`)))
    setTodos(todos.filter(todo => !todo.completed))
  }
  return (
    <>
      <h1>Todoリスト</h1>
      <p>残りのタスク：{todos.filter(todo => !todo.completed).length}</p>
      <input value={inputValue} onChange={handleInputValue} />
      <button onClick={handleAddTodo}>追加</button>
      <button onClick={handleClear}>削除</button>
      <TodoList todos={todos} todoCompleted={todoCompleted} />
    </>
  )
}

export default App;

```
```react:TodoList.jsx
import React from 'react'

const TodoList = ({todos, todoCompleted}) => {
  return (
    <>
      {todos.map((todo) => {
        const getTodoCompleted = () => {
          todoCompleted(todo.id)
        }
        return (
          <div key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={getTodoCompleted}
               />
              {todo.name}
            </label>
          </div>
        )
      })}
    </>
  )
}

export default TodoList
```
## 最後に
意外と長くなってしまった…
もし、これよりもいい書き方等あれば教えていただければ嬉しいです。
ここまで見ていただき、ありがとうございました！