前回のはなし
前回の初投稿でReactを使ってTodoリストを作成を作成しました。
記事はこちら↓
でもこれだと…
リロードすると元に戻ってしまう → 値が保存されていない!!!
そうなるとこのTodoリストを使った人の末路は…
自分「よし、今日のやること書いたぞ!これで頑張ろ!」
...1時間後
自分「あーー、ブラウザのタブ開きすぎてもうよく分からなくなってきた…一旦全部閉じよ」
再びブラウザ開き、作業に入る
自分「そういえば、これ終わったら何するんだっけ?Todoリスト確認するか!」
確認しようとTodoリストを起動
自分「あれ?何も書いていないじゃん!
ってことは、今日のやることはもう終わったのか!!よっしゃーー!アニメでも観よ!」
ってなります。
なので今回は、登録したタスクのデータを保存していこうと思います!
それではレッツゴー〜⭐️
【Rails編 ①】 プロジェクトの作成
rails new todo_api --api
プロジェクトが作成されたら、プロジェクトに移動して(todo_api直下で)、一旦起動できるか確認してみます。
rails s
http://localhost:3000 にアクセスして以下の画面が出たら、成功です!
因みに上の通り、Railsのバージョンは7.1.4、Rubyのバージョンは3.2.2
【Rails編 ②】 ポート設定
一旦、起動したサーバーを切って(control + c)、下記のようにポート番号を書き換えます。
- port ENV.fetch("PORT") { 3000 }
+ port ENV.fetch("PORT") { 3010 }
reactでポート3000番を使用するのでそれ以外の番号に書き換えます。
今回は3010番にしています。
【Rails編 ③】 CORS の設定
何も設定しないと、Reactにアクセスすることができないのでここで設定をしていきます。なのでCORSの設定ができるgemライブラリを使用します↓
gem "rack-cors"
rack-corsという記述が書いてある部分のコメントアウトを外します。
bundle install
インストールできたら、設定を記述していきます↓
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モデルの作成
rails g model Todo
Todoモデルを作り、
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で使います。
rails db:migrate
このコマンドでテーブルが作成されます。
2. コントローラーの作成
コントローラーディレクトリの中に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の部分
createやupdateアクションなどで、リクエストから受け取ったデータを取得するために使用されます。
3. ルーティングの設定
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というライブラリを使用します。
npm install axios
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
先ほどrails側で追加した「◻️タスク」が表示されていれば、通信は成功です⭐️
もし上手くいかない場合は、debuggerを仕込み、処理を止めてres.dataの値を見てみましょう!
【React編 ②】タスクの追加
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編 ③】タスクの状態選択機能作成
//...省略
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編 ④】タスクの削除
//...省略
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 によって、全ての削除リクエストが完了するまで待機しています。
以上の書き換えによってタスクの値をサーバーに保存させることができます。
全ソースコード
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;
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
最後に
意外と長くなってしまった…
もし、これよりもいい書き方等あれば教えていただければ嬉しいです。
ここまで見ていただき、ありがとうございました!