React + Rails のSPAでshowアクションを実装する


これは何?

React+Railsで簡単なタスク管理アプリを作っています。
一覧画面からタスク名のリンクをクリックすると、詳細画面が現れます。

実行環境は以下の通りです。

  • Rails 6.0.3
  • React 17.0.2

また、今回のディレクトリ構成は以下の通りです。(関係のある箇所だけ表示)

.
├── controllers
│   └── api
│       └── tasks_controller.rb
└── javascript
    └── pages
        ├── Task.jsx
        └── Tasks.jsx

Rails側

spaではないRailsのアプリとほぼ変わりありません。/api配下に置いたコントローラーにindex(実装については前回の記事をご参照ください)とshowアクションを記載します。

render json: ...でJSON形式でビューを描画します。

app/controllers/api/tasks_controller.rb
class Api::TasksController < ApplicationController
  def index
    @tasks = current_user.tasks
    render json: @tasks
  end

  def show
    @task = current_user.tasks.find(params[:id])
    render json: @task
  end
end

SPA側(一覧表示画面)

タスク一覧表示画面(Tasksコンポーネント)に実際に書いたコードがこちらです。

app/javascript/pages/Tasks.jsx
import React, {useState} from 'react'
import { Link, withRouter } from 'react-router-dom'
import axios from 'axios'

export const Tasks = withRouter(() => { // ★2解説します
  const [tasks, setTasks] = useState([])

  React.useEffect(async () =>{
    const response = await axios.get('/api/tasks');
    setTasks(response.data)
  }, [])

  return (
    <div>
      <h1>タスク一覧</h1>
      <table>
        // 中略
        <tbody>
          {tasks.map(task => (
            <tr key={task.id}>
              <td>
                <Link to={{ // ★1解説します
                  pathname: "/spa/tasks/" + task.id,
                  state: {id: task.id},
                  }}>{task.name}</Link>
              </td>
              // 中略
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
})

react-router と react-router-dom

まず、調べていて迷ったのが、react-routerreact-router-domがあるっぽいということ。その点は、こちらの記事が非常に丁寧に解説してくれていました。

要するに、react-router-domの方が新しくて使い勝手も良いそうなので、react-router-domを使いましょう、とのことでした。

Link to

ポイントの一つ目はこの部分(★1)で、

<Link to={{
  pathname: "/spa/tasks/" + task.id,
  state: {id: task.id},
}}>{task.name}</Link>

react-router-domLinkという関数で、遷移先や遷移先のページに送るデータなどが指定できます。詳しくは公式のAPIドキュメントがわかりやすかったですが、

上記を指定することで、受取手側のコンポーネントではlocatitonで以下のような情報が取得できました。

console.log(location)
=> {pathname: "/spa/tasks/1", state: {id: 1}, search: "", hash: "", key: "5ox837"}

withRouter

ただし、上記だけではlocationstateundefined になります。調べてみるとwithRouterでラップしてねという回答がたくさん出てきたので、今回も以下のように全体をラップしてみました(★2)。

export const Tasks = withRouter(() => {
 // 略
})

公式のドキュメントもみてみると、

withRouter will pass updated match, location, and history props to the wrapped component whenever it renders.

(withRouterは、レンダリングされるたびに、更新されたmatch、location、およびhistoryのpropsをラップされたコンポーネントに渡します。)

とのことでした。Functional ComponentでのwithRouterの使い方は、こちらを参考にしました。

完成

以上で、実装は終わりになります。
まさかShowアクションでここまで調べることになるとは思わなかった...^^
本日はDestroyアクションも試してみたのですが、そちらはまた別の機会に記事にしたいです。

ではでは。