この記事について
無限スクロールしたいというのは、割と良くあることだと思います。
純粋な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ファイル一つにつき、いくつの動画データを入れるかという意味です。無限スクロール一回につき、いくつの動画を読み込むかという意味にもなります。
なので、無限スクロールできる回数は、ここのskip
、limit
と、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によって生成された画像などを無限スクロールで表示することができます。