LoginSignup
6
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-26

この記事について

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

まず素直に書いてみる

無限スクロールのコンポーネントには、スター数が多いankeetmaini/react-infinite-scroll-componentを使います。

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にはクエリ結果が1つのファイルに収められているので、動画数が10000とかになれば、数MBのファイルになるでしょう。

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

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

解決策

一度に読み込む分のデータを切り分けて、jsonファイルにします。
JSON Outputというめちゃめちゃ便利なGatsbyプラグインがあるので、これを使います。
JSON Outputをインストールしたら、gatsby-config.jsのpluginsをこんな感じで書き換えます。

// あなたのサイトの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ファイル一つにつき、いくつの動画データを入れるかという意味です。無限スクロール一回につき、いくつの動画を読み込むかという意味にもなります。

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

注意点1

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

Gatsby.jsのGraphQLのフラグメントの中身はgatsby-transformer-sharp/src/fragments.jsに書いてあるので、上記のように中身を直接書きましょう。

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

注意点2

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

最初のファイルを書き換える

こんな感じにします。


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によって生成された画像などを無限スクロールで表示することができます。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5