Gatsby.jsで無限スクロールを実装する方法

## この記事について

無限スクロールしたいというのは、割と良くあることだと思います。
純粋なReactだと無限スクロールのコンポーネントはGithubにもたくさん上がっていて、それらを導入するだけで済むのですが、Gatsby.jsを使用する場合にはちょっとした注意点と対策が必要なので、それを書きます。

## まず素直に書いてみる

無限スクロールのコンポーネントには、スター数が多い[ankeetmaini/react-infinite-scroll-component](https://github.com/ankeetmaini/react-infinite-scroll-component)を使います。

```jsx
import React, { useState } from "react"
import { graphql } from "gatsby"
import InfiniteScroll from 'react-infinite-scroll-component'

const IndexPage = ({ data: { allVideo }}) => {
    const [showVideoIndex, setShowVideoIndex] = useState(24)
    const [showVideos, setShowVideos] = useState(allVideo.nodes)

    return (

        <InfiniteScroll
            dataLength={showVideoIndex}
            next={() => setShowVideoIndex(showVideoIndex + 24)}
            hasMore={showVideos.length > showVideoIndex}
        >
            {showVideos.slice(0, showVideoIndex).map((video, key) => (
                <VideoCard video={video}/>
            ))}
        </InfiniteScroll>

    </div>
  )
}

export default IndexPage

export const query = graphql`
{
    allVideo {
        nodes {
            id
            thumbnail_image {
                childImageSharp {
                    fluid(maxWidth: 300) {
                        ...GatsbyImageSharpFluid
                    }
                }
            }
        }
    }
}
`
```

これは、動画のサムネイルを表示して、無限スクロールする例です。
簡単にやっていることを説明すると、`allVideo`というgraphQLを発行し、全ての動画を配列で取得、スクロールするごとに`showVideoIndex`を加算し、`allVideo`配列の最初から`showVideoIndex`までのサムネイルを表示しています。

### 問題点
上記のプログラムには、問題があります。
動画が増えれば増えるほど、このページは読み込みが重くなります。
さらに、上記のページに全てのページからワンクリックでアクセスできる場合、サイト全体が重くなります。

理由は、Gatsby.jsがバックグラウンドで読ませている`page-data.json`にあります。

Gatsby.jsは、まず初期レンダリングの状態を静的なHTMLとして生成します。上記の例だと、初期の`showVideoIndex`は24なので、24個の動画サムネイルが表示される状態のHTMLファイルを生成します。

それと同時に、上記のページで発行しているgraphQLの結果を`page-data.json`として生成しています。
HTMLファイルと一緒にブラウザに渡して、Hydrateしているみたいです。

`page-data.json`にはクエリ結果が１つのファイルに収められているので、動画数が10000とかになれば、数MBのファイルになるでしょう。

さらに、Gatsbyは、Gatsby Link(`<Link/>`)先のページの`page-data.json`もプリフェッチします。
先ほど全てのページが重くなる可能性があると言ったのは、そのためです。

じゃあapiを作って、そこから読み込むというのが選択肢になるわけですが、外部のAPIを使った場合、Gatsbyの生成した画像URLなどは参照できないわけです。

## 解決策
一度に読み込む分のデータを切り分けて、jsonファイルにします。
[JSON Output](https://www.gatsbyjs.com/plugins/gatsby-plugin-json-output/?=json)というめちゃめちゃ便利なGatsbyプラグインがあるので、これを使います。
JSON Outputをインストールしたら、`gatsby-config.js`のpluginsをこんな感じで書き換えます。

```js
// あなたのサイトのURL
const siteUrl = `https://example.com`

module.exports = {
  siteMetadata: {
    ...
  },
  plugins: [
    {
      resolve: `gatsby-plugin-json-output`,
      options: {
        siteUrl: siteUrl,
        graphQLQuery: `
        {
          allVideo(skip: 24, limit: 240) {
            nodes {
              id
              thumbnail_image {
                  childImageSharp {
                      fluid {
                        aspectRatio
                        src
                        srcSet
                        sizes
                      }
                  }
              }
            }
          }
        }`,
        serialize: results => results.data.allVideoOrderByReleasedAt.nodes.map(_ => ({
          path: ``, // ファイルを生成する先のパス
        })),
        serializeFeed: results => results.data.allVideo.nodes,
        feedFilename: `all_video`,
        nodesPerFeedFile: 24, // 一回に読み込む数
      }
    },
  ],
}
```
これで、jsonファイルが`/all_video-1.json`、`/all_video-2.json`...みたいな感じで生成されます。

`allVideo(skip: 24, limit: 240)`としているのは、最初のプログラムで書いた通り、24個の動画を初期レンダリングさせるつもりなので、最初の24個はjsonデータを生成しなくて良いので、スキップさせています。

`nodesPerFeedFile`は、jsonファイル一つにつき、いくつの動画データを入れるかという意味です。無限スクロール一回につき、いくつの動画を読み込むかという意味にもなります。

なので、無限スクロールできる回数は、ここの`skip`、`limit`と、`nodesPerFeedFile`の値で決まります。`(limit - skip) / nodesPerFeedFile = 無限スクロール可能回数`です。余りが出る可能性がある場合は、例外が発生しないように気をつけてください。

### 注意点１
`gatsby-config.js`では、GraphQLのフラグメントが使えません。`...GatsbyImageSharpFluid`とかです。

Gatsby.jsのGraphQLのフラグメントの中身は[gatsby-transformer-sharp/src/fragments.js](https://github.com/gatsbyjs/gatsby/blob/10dfe011c368e28e8de19f0f5569748ebb9a6bc3/packages/gatsby-transformer-sharp/src/fragments.js)に書いてあるので、上記のように中身を直接書きましょう。

`base64`というのは、画像が読み込まれるまでに表示されるモヤモヤっとしたモザイク画像みたいなのを表示するためのものですが、jsonファイルのサイズが２倍近くになる可能性があるので、いらないと思います。

### 注意点２
ここで設定したjsonファイル生成は、`gatsby develop`では機能しません。`gatsby build`で生成してください。
一回`gatsby build`で生成してしまえば、`gatsby develop`でフェッチできます。

## 最初のファイルを書き換える
こんな感じにします。

```jsx

import React, { useState } from "react"
import { graphql } from "gatsby"
import InfiniteScroll from 'react-infinite-scroll-component'

const IndexPage = ({ data: { allVideo }}) => {
    const [page, setPage] = useState(0)
    const [videos, setVideos] = useState(allVideo.nodes)
    const [hasMore, setHasMore] = useState(true)

    const addVideos = (newVideos) => {
        const videosCopy = JSON.parse(JSON.stringify(videos))
        videosCopy.push(...newVideos)
        setVideos(videosCopy)
    }

    const fetchVideos = (nextPage) => {
        fetch(`/all_video-${nextPage}.json`)
            .then(response => response.json())
            .then(json => addVideos(json.items))
            .then(() => setPage(nextPage))
            .catch(e => {
                // 取得しようとしたjsonファイルが存在しなかった → もう追加データがない
                setHasMore(true)
                console.log(e)
            })
    }

    return (
    <div className='w-full'>
        <InfiniteScroll
            dataLength={videos.length}
            next={() => fetchVideos(page + 1)}
            hasMore={hasMore}
        >
            {videos.map((video, key) => (
                <VideoCard video={video}/>
            ))}
        </InfiniteScroll>

    </div>
  )
}

export default IndexPage

export const query = graphql`
{
    # limitはgatsby-config.jsに依存
    allVideo(limit: 24) {
        nodes {
            id
            thumbnail_image {
                childImageSharp {
                    fluid {
                        ...GatsbyImageSharpFluid
                    }
                }
            }
        }
    }
}
`
```
スクロールされたら、スクロール回数に応じたjsonファイルをフェッチし、フェッチできなかったら(`catch`されたら）`hasMore=false`にしちゃう感じです。

graphQLのlimit数が`gatsby-config.js`に依存しちゃうので、注意しましょう。（ここのlimit数が10で`gatsby-config.js`のskip数が20とかになると、11個目〜20個目が表示されません。）

これでどれだけ動画数が増えても重くなりませんし、Gatsbyによって生成された画像などを無限スクロールで表示することができます。