【Reactで】小説投稿サイトなどに良くある「全○話」みたいなのを表示させる方法

## はじめに
今回は小説投稿サイトなどによくある「全○話」を自作アプリにて表示させるのに結構苦労した（3日かかった）ので、戒めとして残しておきたいと思います。


#### 実現したかったこと

* 用意したのは`「シリーズ」`というフォルダ的な役割を持つモデルと、`「アイテム」`というシリーズに複数個格納されるモデルの2つ（1対多の関係）

* `ルートページ`にて「シリーズ」全件を表示させ、その`「シリーズ」が所有する「アイテム」を全て取得し、その総数をカウント`させ`「全〜件」という形で表示`させたい。

#### 苦労した理由

* 表示させたいのが`ルートページ`だったからです。普通なら`各シリーズが所有するアイテム`を取得しようとする場合、例えばURLが`"/series/104"`なら、シリーズのパラメータ（この場合なら104）を取得して、そのパラメータを頼りにアイテムを取得します。

　 なので、パラメータが存在しないルートページでどうやって各シリーズのパラメータを取得すりゃええんじゃいとと半ばキレかけながら考えていたわけです（今思えば単純な話でした）

## 環境・前提等


### 環境


* フロントエンド
 * React（v16.8以上）
 * React Hooks（カスタムフックを使う）
 * axios
<br>

* バックエンド
 * Rails（5.2系）

### 前提
* CORSの設定、モデル作成などの工程は省略します。
* PUMAでRails側のローカルホストをデフォルトで3001に指定しています。

### Rails側

* モデル

![スクリーンショット 2020-09-21 16.29.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/7a3f952e-8a4b-9c17-59af-3c112bc24f23.png)

* コントローラ
 * `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側のコード

#### ルーティング

```ruby: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
```

#### コントローラ

```ruby: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側のコード

```react
// 階層

//src
//  ├ Home.js
//  ├ Series.js
//  ├ ItemCount.js
//  └ useFetch.js
```

#### useFetchカスタムフック

```react: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コンポーネント

```react: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コンポーネント

```react: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コンポーネント

```react: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

```