0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

前回のはなし:page_facing_up:

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

記事はこちら↓

でもこれだと…

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

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

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

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

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

ターミナル
rails new todo_api --api

プロジェクトが作成されたら、プロジェクトに移動して(todo_api直下で)、一旦起動できるか確認してみます。

ターミナル(todo_api)
rails s

http://localhost:3000 にアクセスして以下の画面が出たら、成功です!

スクリーンショット 2024-10-08 21.21.06.png

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

【Rails編 ②】 ポート設定

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

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

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

【Rails編 ③】 CORS の設定

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

Gemfile
gem "rack-cors"

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

ターミナル(todo_api)
bundle install

インストールできたら、設定を記述していきます↓

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モデルの作成

ターミナル(todo_api)
rails g model Todo

Todoモデルを作り、

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で使います。

ターミナル(todo_api)
rails db:migrate

このコマンドでテーブルが作成されます。

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

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

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 を返します。
status: :unprocessable_entityがなかったら?
※バリデーションエラーが来ても200 成功したコードが返される可能性があります。

def update

endの部分

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

def destroy

endの部分

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

private

endの部分

createupdateアクションなどで、リクエストから受け取ったデータを取得するために使用されます。

3. ルーティングの設定

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つ作成しておきます。

ターミナル
rails c

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

【React編 ①】タスクの取得

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

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

todo-app
npm install axios
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

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

【React編 ②】タスクの追加

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編 ③】タスクの状態選択機能作成

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編 ④】タスクの削除

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

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

全ソースコード

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;

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

最後に

意外と長くなってしまった…
もし、これよりもいい書き方等あれば教えていただければ嬉しいです。
ここまで見ていただき、ありがとうございました!

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?