はじめに
今回は小説投稿サイトなどによくある「全○話」を自作アプリにて表示させるのに結構苦労した(3日かかった)ので、戒めとして残しておきたいと思います。
実現したかったこと
-
用意したのは
「シリーズ」
というフォルダ的な役割を持つモデルと、「アイテム」
というシリーズに複数個格納されるモデルの2つ(1対多の関係) -
ルートページ
にて「シリーズ」全件を表示させ、その「シリーズ」が所有する「アイテム」を全て取得し、その総数をカウント
させ「全〜件」という形で表示
させたい。
苦労した理由
- 表示させたいのが
ルートページ
だったからです。普通なら各シリーズが所有するアイテム
を取得しようとする場合、例えばURLが"/series/104"
なら、シリーズのパラメータ(この場合なら104)を取得して、そのパラメータを頼りにアイテムを取得します。
なので、パラメータが存在しないルートページでどうやって各シリーズのパラメータを取得すりゃええんじゃいとと半ばキレかけながら考えていたわけです(今思えば単純な話でした)
環境・前提等
環境
- フロントエンド
- React(v16.8以上)
- React Hooks(カスタムフックを使う)
- axios
- バックエンド
- Rails(5.2系)
前提
- CORSの設定、モデル作成などの工程は省略します。
- PUMAでRails側のローカルホストをデフォルトで3001に指定しています。
Rails側
- モデル
-
コントローラ
-
Api::V1::SeriesController
-
このコントローラにてシリーズ全件を返すアクションと、アイテムのカウントを返すアクションを作成する。
-
ルーティング
-
ルート:
"/"
→"api/v1/series#index"
-
アイテム取得:
"api/v1/item_count/:series_id"
→"api/v1/series#item_count"
React側
- 用意するコンポーネント
-
Homeコンポーネント
: シリーズを全件取得し、Seriesというコンポーネントに各データを順繰り渡す役割りを持たせる。 -
Seriesコンポーネント
: このコンポーネントにて各シリーズを表示させる。 -
ItemCountコンポーネント
: 各シリーズが持つアイテムの総数だけを表示させる。 -
useFetchカスタムフック
: Railsからデータを取得する。
Rails側のコード
ルーティング
routes.rb
Rails.application.routes.draw do
# ルート
root to: 'api/v1/series#index'
# アイテムのカウント
get 'api/v1/item_count/:id', to: 'api/v1/series#item_count'
end
コントローラ
app/controller/api/v1/series_controller.rb
class Api::V1::SeriesController < ApplicationController
# item_countアクションに、パラメータから取得したシリーズをコールバック
before_action :set_series, only: [:item_count]
def index
@series =Series.all
render json: {
status: 200,
series: @series,
keyword: "index_of_series" # React側で使う
}
end
def item_count
@items = @series.items.all # シリーズに関連付けられているアイテムの取得
@items_count = @items.count # アイテムの総数をカウント
render json: {
status: 200,
item_count: @item_count, # カウントをJSONとしてReactへ送信
keyword: "item_count" # React側で使う
}
end
private
# パラメータを頼りにシリーズを取得
def set_series
@series = Series.find(params[:id])
end
end
React側のコード
// 階層
//src
// ├ Home.js
// ├ Series.js
// ├ ItemCount.js
// └ useFetch.js
useFetchカスタムフック
src/useFetch.js
import { useState, useEffect } from "react"
import axios from 'axios'
// カスタムフックでは文頭はuseが必須
// useFetchの引数に、methodとurlを渡す
// これは、HomeとItemCountコンポーネントにて、Railsとの通信に使う
// HTTPリクエストと、ルーティングを指定するため
export default function useFetch({method, url}) {
// 初期値の定義。
const [items, setItems] = useState("")
useEffect(() => {
const getItems = () => {
// ここのmethodとurlにて、Home・ItemCountコンポーネントから
// 送られてくるメソッドとルーティングを代入することになる。
axios[method](url)
.then(response => {
let res = response.data
let ok = res.status === 200
// シリーズ全件取得
// Rails側で指定したkeywordはここで使う。
// そうしてカウントとの区別を付けている。
if (ok && key === 'index_of_series') {
setItems({ ...res.series })
// シリーズごとのアイテムの総数を取得
} else if (ok && key === 'item_count') {
setItems(res.item_count)
}
})
.catch(error => console.log(error))
}
getItems()
}, [method, url, items])
return {
items // items変数を他のコンポーネントで使えるようにする。
}
}
Homeコンポーネント
src/Home.js
import React from 'react'
import Series from './Series'
import useFetch from './useFetch'
function Home() {
// ここでは、useFetchからRailsで取得したシリーズのデータを受け取っている。
// methodはget、urlはRailsのルートのURLを指定。これにより、
// useFetchからRailsのルートのルーティングへリクエストが送信され、
// その後Railsから受け取ったデータをitemsへ格納します。
const { items } = useFetch({
method: "get",
url: 'http://localhost:3001'
})
return (
<div>
{/* Object.keys()メソッドを使い、JSONで送られてくるitemsを */}
{/* ループ処理で1個ずつSeriesコンポーネントに渡している。 */}
{/* JSONは、{ {...}, {...}, {...} }のようなものであると想定 */}
{Object.keys(items).map(key => (
<Series key={key} items={items[key]} />
))}
</div>
)
}
export default Home
Seriesコンポーネント
src/Series.js
import React from 'react'
import ItemCount from './ItemCount'
function Series(props) {
// Homeから送られてくるpropsを頼りに、各シリーズのidをここで取得しています。
// このidをパラメータとして使うことで、各シリーズの所有するアイテムにアクセスすることができます。
const seriesId = props.items.id
const seriesTitle = props.items.title
return (
<div>
<div>{seriesTitle}</div>
{/* ItemCountコンポーネントに、シリーズのidを渡す。 */}
<ItemCount {...props} seriesId={seriesId} />
</div>
)
}
export default Series
ItemCountコンポーネント
src/ItemCount.js
import React from 'react'
import useFetch from './useFetch'
function SeriesCount(props) {
// useFetchを使いRailsと通信。
// methodはget、urlはRailsの`api/v1/item_count/${props.seriesId}`を指定。
// id部分にSeriesコンポーネントから渡ってくる各シリーズのidを嵌め込むことで、
// Railsの"api/v1/item_count/:id"というルーティングへリクエストが送信され、
// その後Railsから各シリーズの持つアイテムのカウント数を受け取り、最後にitemsへ格納されます。
const { items } = useFetch({
method: 'get',
url: `http://localhost:3001/api/v1/item_count/${props.seriesId} `
})
return (
<div>
{/* Railsから送られてくるアイテムの総数をここにレンダリングします。 */}
(このシリーズは全部で {items} 個のアイテムを所有しています)
</div>
)
}
export default SeriesCount