Rails APIモードとReact Hooksを使ってToDoリストを作る

12 min読了の目安(約11000字TECH技術記事

目的

React hooksの学習、API通信について学習のために作成。
自己の理解を深めるために、記事にする。

前提

Railsの立ち上げまでは、駆け足で割愛します。
学習中の身のため、間違いがある可能性大
もし、目に止まればご指摘いただけると幸いです。

アウトライン

  1. Rails APIを作る
  2. Reactの開発環境を整える
  3. Createを作る
  4. Readを作る
  5. updateを作る
  6. Deleteを作る

環境

Rails 6.0.3.4
React 17.0.1

Rails APIを作る

Rails APIを立ち上げる

console
$ 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しちゃいます。

seeds.rb
Issue.create([
  {name: "点"},
  {name: "線"},
  {name: "面"}
])
console
$ rake db:seed

コントローラを作る

console
$ rails g controller issues

issues_controller.rbを下記のように書きます。
Json形式で返してあげるように書きます。

issues_controller.rb
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を整えて

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に下記を追記します。

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

先に完成したコードを記載

App.js
import React from 'react';
import Todo from './components/Todo'
import './App.css';

export default function App() {
  return (
    <div className="App">
      <Todo/>
    </div>
  )
}
components/Todo.js
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とかにコンポーネント分けをやっていこうと思います。
でき上がったらまた別の記事にします。

ezgif.com-gif-maker.gif

参考にした記事