Rails APIモードとReact Hooksを使ってToDoリストを作る
目的
React hooksの学習、API通信について学習のために作成。
自己の理解を深めるために、記事にする。
前提
Railsの立ち上げまでは、駆け足で割愛します。
学習中の身のため、間違いがある可能性大
もし、目に止まればご指摘いただけると幸いです。
アウトライン
- Rails APIを作る
- Reactの開発環境を整える
- Createを作る
- Readを作る
- updateを作る
- Deleteを作る
環境
Rails 6.0.3.4
React 17.0.1
Rails APIを作る
Rails APIを立ち上げる
$ rails new app -d mysql --api
$ cd app
GemfileのRack-cors
のコメントアウトを解除する
$ bundle install
$ rails g model issue name:text
$ rake db:create
$ rake db:migrate
適当にseedsしちゃいます。
Issue.create([
{name: "点"},
{name: "線"},
{name: "面"}
])
$ rake db:seed
コントローラを作る
$ rails g controller issues
issues_controller.rb
を下記のように書きます。
Json形式で返してあげるように書きます。
class IssuesController < ApplicationController
def index
@issue = Issue.all
render json: @issue
end
def create
@issue = Issue.create(name: params[:name])
render json: @issue
end
def update
@issue = Issue.find(params[:id])
@issue.update_attributes(name: params[:name])
render json: @issue
end
def destroy
@issue = Issue.find(params[:id])
if @issue.destroy
head :no_content, status: :ok
else
render json: @issue.errors, status: :unprocessable_entity
end
end
end
routes.rb
を整えて
Rails.application.routes.draw do
resources :issues
end
railsを立ち上げてJSONが返ってくるか確認
$ rails s -p 3001
下記のどちらでもいいのでJSONが返ってきていることを確認する
- コンソールで
curl -g localhost:3001/issues/
- ブラウザで
localhost:3001/issues/
[{"id":1,"name":"点","created_at":"2020-12-12T07:50:20.293Z","updated_at":"2020-12-12T07:50:20.293Z"},{"id":2,"name":"線","created_at":"2020-12-12T07:50:20.298Z","updated_at":"2020-12-12T07:50:20.298Z"},{"id":3,"name":"面","created_at":"2020-12-12T07:50:20.302Z","updated_at":"2020-12-12T07:50:20.302Z"}]
このあと、Rails側3000番ポートに、Reactがアクセスするので、3000番ポートへのアクセスを許可します。application.rb
に下記を追記します。
・
・
・
config.api_only = true
config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000'
resource '*',
:headers => :any,
:methods => [:get, :post, :patch, :delete, :options]
end
end
Rails側の設定はこれで終わり!
React側の開発環境を整える
react-create-appを使う
下記コマンドを使用して、「react-create-app」を使えるようにする
$ npm install -g create-react-app
appディレクトリ
上で下記コマンドを実行する
todo_frontというディレクトリと付随するものが作成される
$ create-react-app todo_front
下記のようなディレクトリ構成にするために、componentsディレクトリとTodo.jsを作成する
.
├── App.css
├── App.js
├── App.test.js
├── components
│ └── Todo.js
├── index.css
├── index.js
├── logo.svg
└── registerServiceWorker.js
App.js
を下記のように書き換え、componentsディレクトリ内のTodo.js
を読み込むようにする
(このあと、拡張していこうと思いまして...)
Todo.js
にRailsと通信するAjax部分を記述していく。
今回はAPIと通信する便利なライブラリaxiosを使用する。
ついでに、cssフレームワークにmaterial-ui
もインストールする
$ npm install axios
$ npm install @material-ui/core
先に完成したコードを記載
import React from 'react';
import Todo from './components/Todo'
import './App.css';
export default function App() {
return (
<div className="App">
<Todo/>
</div>
)
}
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
Button,
Container,
CssBaseline,
List,
ListItem,
ListItemText,
Input,
ListItemSecondaryAction,
Checkbox
} from '@material-ui/core';
export default function MainContainer () {
const [createissue, setCreateissue] = useState("");
const [issues, setIssues] = useState([]);
const [updateissue, setUpdateissue] = useState("");
const createIssue = (event) => {
console.log("イベント発火")
axios.post('http://localhost:3001/issues',
{
name: createissue
}
).then(response => {
console.log("registration response", response.data)
setIssues([...issues, {
id: response.data.id,
name: response.data.name
}])
resetTextField()
}).catch(error => {
console.log("registration error", error)
}).catch(data => {
console.log(data)
})
event.preventDefault()
}
useEffect(() => {
async function fetchData() {
const result = await axios.get('http://localhost:3001/issues',)
console.log(result)
console.log(result.data)
setIssues(result.data);
}
fetchData();
}, []);
const deleteIssue = (id) => {
axios.delete(`http://localhost:3001/issues/${id}`)
.then(response => {
setIssues(issues.filter(x => x.id !== id))
console.log("set")
}).catch(data => {
console.log(data)
})
}
const updateIssue = (id) => {
axios.patch(`http://localhost:3001/issues/${id}`,
{
name: updateissue
}
).then(response => {
setIssues(issues.filter(x => x.id !== id))
console.log(response.data)
}).catch(data => {
console.log(data)
})
}
const resetTextField = () => {
setCreateissue('')
}
const handleUpdate = (event) => {
setUpdateissue(event.target.value)
}
return (
<React.Fragment>
<Container component='main' maxWidth='xs'>
<CssBaseline/>
<form onSubmit={createIssue}>
<Input
type="text"
name="issue"
value={createissue}
placeholder="Enter text"
onChange={event => setCreateissue(event.target.value)}
/>
<Button
type="submit"
variant='contained'
color='primary'>
つぶやく
</Button>
</form>
<List
style={{marginTop: '48px'}}
component='ul'
>
{issues.map(item => (
<ListItem key={item.id} component='li' >
<Checkbox
value='primary'
onChange={() => deleteIssue(item.id)}
/>
<ListItemText>
ID:[{item.id}]
Name:[{item.name}]
</ListItemText>
<ListItemSecondaryAction>
<form>
<Input
type="text"
name="issue"
value={updateissue}
onChange={event => handleUpdate(event)}
/>
<Button
type="submit"
onClick={() => updateIssue(item.id)}
>
更新
</Button>
</form>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Container>
</React.Fragment>
);
}
Createを作る
新しいissueを作成するフォームを作る
useStateを使用して、フォームで入力したデータをcreateissueとして一時保持させる
const [createissue, setCreateissue] = useState("");
インプットフォームに入力するとvalueがデータを持ちます。
そのデータを保持させるために、setStateで作ったcreateissue
を使います。
そして、「つぶやく」ボタンを押すとonSubmitでcreateIssue
が発火します。
<form onSubmit={createIssue}>
<Input
type="text"
name="issue"
value={createissue}
placeholder="Enter text"
onChange={event => setCreateissue(event.target.value)}
/>
<Button
type="submit"
variant='contained'
color='primary'>
つぶやく
</Button>
</form>
作成した新しいissueをaxios経由でDBへ書き込む
createIssue
が発火するとlocalhost:3001/issuesにpostします!(そのまんま)
issueのnameに先ほど入力し保持したcreateissue
として渡します。
無事、postに成功したらresponseにデータを保持させて返ってきます。
そして、インプットフォームを空欄に戻すためにresetTextField
を組んで、setCreateissueを空にします。
axiosについてはaxiosを調べた方が詳しいので割愛!
const createIssue = (event) => {
console.log("イベント発火")
axios.post('http://localhost:3001/issues',
{
name: createissue
}
).then(response => {
console.log("registration response", response.data)
setIssues([...issues, {
id: response.data.id,
name: response.data.name
}])
resetTextField()
}).catch(error => {
console.log("registration error", error)
}).catch(data => {
console.log(data)
})
event.preventDefault()
}
const resetTextField = () => {
setCreateissue('')
}
Readを作る
すでにCreateに登場したが、useStateを使ってissuesにReactで表示するデータを保持させる。
const [issues, setIssues] = useState([]);
useEffectを使ってレンダーされる度に、getを飛ばすように設定する。
そして、第2引数に[]を渡して再度レンダリングされないようにする
ここら辺の理解が曖昧
useEffect(() => {
async function fetchData() {
const result = await axios.get('http://localhost:3001/issues',)
console.log(result)
console.log(result.data)
setIssues(result.data);
}
fetchData();
}, []);
mapを使って、issues内にある
{issues.map(item => (
<ListItem key={item.id} component='li' >
<ListItemText>
ID:[{item.id}]
Name:[{item.name}]
</ListItemText>
Updateを作る
要領はCreateと同じ。
updateIssue関数に「更新したいid」と「入力したvalue」を渡す
const [updateissue, setUpdateissue] = useState("");
<ListItemSecondaryAction>
<form>
<Input
type="text"
name="issue"
value={updateissue}
onChange={event => handleUpdate(event)}
/>
<Button
type="submit"
onClick={() => updateIssue(item.id)}
>
更新
</Button>
</form>
</ListItemSecondaryAction>
渡されたidのissueのnameを入力したupdateissueに変更するよう、railsへ送る
const updateIssue = (id) => {
axios.patch(`http://localhost:3001/issues/${id}`,
{
name: updateissue
}
).then(response => {
setIssues(issues.filter(x => x.id !== id))
console.log(response.data)
}).catch(data => {
console.log(data)
})
}
Deleteを作る
updateの要領と大体同じですが、
私の場合はチェックボックスにチェックを入れたら、deleteIssueが発火するようにしています。
発火したらIdとdeleteがRailsに送られ、削除するようになっています。
削除が成功したらの信号が返ってきたら、フロント側の表示リストを書き換えます。
<Checkbox
value='primary'
onChange={() => deleteIssue(item.id)}
/>
const deleteIssue = (id) => {
axios.delete(`http://localhost:3001/issues/${id}`)
.then(response => {
setIssues(issues.filter(x => x.id !== id))
console.log("set")
}).catch(data => {
console.log(data)
})
}
最後に
参考にさせてもらった記事をベースにHooksへ書き換えてみました。
しかし、1つのコンポーネントへ冗長に書いてしまったので、他のhooksの学習含めてForm, Viewとかにコンポーネント分けをやっていこうと思います。
でき上がったらまた別の記事にします。
参考にした記事
Discussion