SWR活用パターン 無限スクロール編 (+ graphql-request, 交差オブザーバー APIカスタムフック)
できたもの
動作的には、uhyo 氏の以下の記事で紹介されたいたものと同じである。
Recoil selector活用パターン 無限スクロール編
- サーバーから取得したデータがリストで表示されている。
- ユーザーがスクロールしてリストの下に到達したら、サーバーから追加のデータを読み込んでリストの下に継ぎ足す。[1]
uhyo 氏の例と同じく、今回も1回50件ずつ取得している。
GitHub リポジトリ
この記事の要旨
- SWR には、無限ローディング(無限ローディング)を実現することを想定した API がある。
- それは、
useSWRInfinite
である。 - ある意味、無限ローディング専用の API なので
useSWRInfinite
を使うと、シンプルに無限ローディングが実装できる。 -
useSWRInfinite
では、GraphQL も利用可能。
- それは、
- 無限ローディングで、交差オブザーバー API (Intersection Observer API) を利用するときには、カスタムフックを作ると便利である。
- 具体的には、交差が起きると
true
を返すようなカスタムフックを作る。
- 具体的には、交差が起きると
- 無限ローディングを実装するには、交差オブザーバー API のカスタムフックが
true
を返したときに、React のuseEffect
を使って、useSWRInfinite
で新しいページのリクエストを走るようにすればよい。
この記事を書こうと思った経緯
uhyo 氏の「Recoil selector 活用パターン 無限スクロール編」を読んだ。
そして、漠然とだが、SWR で作ったらどうなるのだろうかと思った。
そこで、作ってみた、というのがこの記事である。
最初は、あまり調べずに我流で作っていた。
だが、Twitter で、「swr のドキュメントに全てが書いてある」のようなツイートを見かけたことを思い出し、SWR の公式ドキュメントを読んでみた。
そうしたら、公式に「無限ローディングには、useSWRInfinite
という API を使え」と書いているではないか。
API 名からして、useSWR≪Infinite(無限)≫である。
SWR は、ページネーションや無限ローディングなどの一般的な UI パターンをサポートするための専用 API である useSWRInfinite を提供しています。
infinite-loading 公式の GitHub issue のタイトルを取得するサンプル
また、「useSWRInfinite SWR」とかでググると、Zenn や個人ブログにも記事があった。
SWRのuseSWRInfinite()を使って無限ローディングを実装する りーほーブログ
useSWRInfiniteを使うとページング(無限ローディング)の処理がシンプルに書けて気持ちいい! Zenn
この時点で、「もう記事があるから、書かなくてもいいか……」と思った。しかし、
- ✅
useSWRInfinite
で GraphQL を使う。 - ✅ 交差オブザーバー API (Intersection Observer API) を最新ページ読み込みのトリガーとする。
のふたつを満たすものが、私の調べる限りなかったので、やはり作ってみることにした。
また、「記事を読んで理解する」のと、「自分で実際に動くものが作れる」は、違うので、
自分で実際に動くものと作ってみようという意味もあった。
なお、「無限スクロール」「無限ローディング」などの名称があるが、
この記事では、「無限ローディング」で呼び方を固定する。
バージョン情報/環境
バージョン情報/環境
Windows Home 11 / WSL / Ubuntu 20.04
Node.js 18.13.0
pnpm 7.27.1
graphql 16.6.0
graphql-request 5.1.0
react 18.2.0
react-dom 18.2.0
swr 2.0.3
ディレクトリ構成
ディレクトリ構成
.
├── dist
├── index.html
├── package.json
├── personal.code-workspace
├── pnpm-lock.yaml
├── public
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── features
│ │ └── inifiniteLoading
│ │ ├── components
│ │ │ ├── Loading
│ │ │ │ └── index.tsx
│ │ │ └── PokemonList
│ │ │ └── index.tsx
│ │ ├── customHooks
│ │ │ ├── useIsIntersecting.ts
│ │ │ └── usePokemonListSWRInfinite.ts
│ │ └── useCases
│ │ └── formattedPokemonListQuery.ts
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── tsconfig.eslint.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
いかにして無限ローディングを実現するか
基本事項の確認 PokeAPI の仕様
まず、対象となる ポケモンの API、PokeAPI の仕様について確認したい。
これは、公式の、Resource Lists/Pagination (group) に載っていた。
まず、1回のリクエストで取得するリソース数(件数)、これは、limit
で指定できる。
そして、ポケモンの ID の何番から、取得するか。
これは、offset
で、取得したい ID の直前の数値を指定する。
例えば、ポケモン ID の151番から、30件取得したいとする。
その場合、offset
には150、limit
には30を指定して、リクエストすればよい。
実装の考え方 無限ローディングをページネーションと捉える
実装の考え方は、無限ローディングを一種のページネーションと考えることだ。
ユーザーがスクロールしてリストの下に到達したら、ポケモンのデータを50件ずつ取得する。
つまり、
1ページ目 0-50
2ページ目 51-100
3ページ目 101-150
……
のようになる。
ここで、最初の1ページを index の0
と考える。
すると、offset
(取得する直前の ID)は、そこからの index に取得したい件数(今回は50件)を掛けた値である。
図にすると以下のようになる。
したがって、無限ローディングを実現するには、
- ページの index (最初の1ページを0とする)
- ページの index を
+1
する関数
が必要だ。
以上を実現するのが、SWR の useSWRInfinite
である。
import useSWRInfinite from 'swr/infinite'
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
ここで SWR 自体については、公式サイトの解説や以前私の書いた記事を参照されたい。
以下、簡単に useSWRInfinite
の API の説明をする。
引数の getKey
は、ページのインデックスと前ページのデータを受け取る関数で、ページのキーを返す。
ページのキーとは、fetcher
にわたされる API エンドポイントなどのデータである。
getKey
内で、受け取るページのインデックスを増やすには、戻り値の size
、setSize
を使う。
size
は、フェッチして返されるだろうページ数。
setSize
は、フェッチする必要のあるページ数を設定する。
size
と getKey
内で受け取れるインデックスは連動している。
getKey
関数内で受け取るインデックスをインクリメント、つまり、1
増やすには、フェッチする必要のあるページ数を 1
増やせばよい。
コードで言うと、setSize(size + 1)
とすればよい。
ここで、「いかにして無限ローディングを実現するか」についてまとめる。
まず、データの取得については、getKey
でインデックス0のページのデータ、インデックス1のページぞれぞれのデータを動的に取得する。
インデックスをインクリメントするには、setSize(size + 1)
を実行する。
setSize(size + 1)
を実行するトリガーは、Loading
コンポーネントが表示されることである。
表示の判定は、JavaScript の交差オブザーバー API を使い、setSize(size + 1)
の実行は、React の useEffect
を使う。
これによって、新しいページのリクエストが走る。
というのが一連の流れになる。
以上を踏まえて、実際のコードを見ていこう。
useSWRInfinite カスタムフック(ポケモンデータを取得)
import { request } from 'graphql-request'
import useSWRInfinite from 'swr/infinite'
import {
convertRawResultToPokemon,
type Pokemon
} from '@/features/inifiniteLoading/useCases/formattedPokemonListQuery'
/**
* @typedef API のレスポンス
*/
export interface QueryResult {
species: ReadonlyArray<{
readonly name: string
readonly id: number
readonly pokemon_v2_pokemonspeciesnames: ReadonlyArray<{
language_id: 9 | 11
name: string
}>
}>
}
/**
* @description GraphQL のクエリ
*/
const query = `
query ($offset: Int!, $limit: Int!) {
species: pokemon_v2_pokemonspecies(
where: {}
order_by: { id: asc }
offset: $offset
limit: $limit
) {
name
id
pokemon_v2_pokemonspeciesnames(where: { language_id: { _in: [9, 11] } }) {
language_id
name
}
}
}
`
/**
* @typedef GraphQL のクエリの変数の型
*/
interface PokemonGraphqlVariables {
offset: number
limit: number
}
/**
* @typedef getKey の返り値の型
*/
type ApiKeyType = [string, PokemonGraphqlVariables]
/**
* @description 1ページあたりに取得するポケモンデータの件数
*/
const MAX_NUMBER_OF_ENTRY_BY_QUERY = 50
/**
* @description useSWRInfinite の引数の getKey
*/
const getKey = (
pageIndex: number,
previousPageData: Pokemon[] | null
): ApiKeyType | null => {
if (previousPageData !== null && previousPageData.length === 0) {
return null
}
const pageStartPoint = pageIndex * MAX_NUMBER_OF_ENTRY_BY_QUERY
return [
query,
{
offset: pageStartPoint,
limit: MAX_NUMBER_OF_ENTRY_BY_QUERY
}
]
}
/**
* @description useSWRInfinite の引数の fetcher
*/
const fetcher = async (
query: string,
{ offset: offsetVal, limit: limitVal }: PokemonGraphqlVariables
): Promise<Pokemon[]> => {
const response = await request<QueryResult>(
'https://beta.pokeapi.co/graphql/v1beta',
query,
{
offset: offsetVal,
limit: limitVal
}
)
return convertRawResultToPokemon(response)
}
/**
* @typedef usePokemonListSWRInfinite の戻り値の型
*/
interface UsePokemonListSWRInfiniteReturnType {
pokemonsDataArr: Array<readonly Pokemon[]>
error: Error | undefined
isLoading: boolean
loadMorePage: () => void
isReachingEnd: boolean
}
/**
* @description
* useSWRInfinite でポケモンデータを取得する
* ページサイズをインクリメントする関数を返す
* など。
*/
export const usePokemonListSWRInfinite =
(): UsePokemonListSWRInfiniteReturnType => {
const { data, error, isLoading, size, setSize } = useSWRInfinite<
Pokemon[],
Error
>(
getKey,
async ([query, pokemonGraphqlVariables]: ApiKeyType) =>
await fetcher(query, pokemonGraphqlVariables),
{ revalidateFirstPage: false }
)
// すべてのデータを取得したら、true
const isReachingEnd: boolean =
data !== undefined &&
data[data.length - 1]?.length < MAX_NUMBER_OF_ENTRY_BY_QUERY
const emptyData: Array<readonly Pokemon[]> = []
const pokemonsDataArr: Array<readonly Pokemon[]> =
typeof data === 'undefined' ? emptyData : data
// ページを増やす => 最新のページを読み込むtrigger
const loadMorePage = (): void => {
void setSize(size + 1)
}
return {
pokemonsDataArr,
error,
isLoading,
loadMorePage,
isReachingEnd
}
}
以下、ポイントごとに解説する
getKey
/**
* @typedef getKey の返り値の型
*/
type ApiKeyType = [string, PokemonGraphqlVariables]
/**
* @description 1ページあたりに取得するポケモンデータの件数
*/
const MAX_NUMBER_OF_ENTRY_BY_QUERY = 50
/**
* @description useSWRInfinite の引数の getKey
*/
const getKey = (
pageIndex: number,
previousPageData: Pokemon[] | null
): ApiKeyType | null => {
if (previousPageData !== null && previousPageData.length === 0) {
return null
}
const pageStartPoint = pageIndex * MAX_NUMBER_OF_ENTRY_BY_QUERY
return [
query,
{
offset: pageStartPoint,
limit: MAX_NUMBER_OF_ENTRY_BY_QUERY
}
]
}
上にも書いたように、getKey
はページのインデックスと前ページのデータを受け取る関数で、ページのキーを返す関数である。
返された値は、後述する fetcher
に渡される。
公式ドキュメントにもあるように、null
を返した場合は、fetcher
でのリクエストは行われない。
ここでは、読み込むデータが、もうない(データを読み込み、ポケモンリストの最後に到達した)場合、に null
を返すようにしている。
ページのインデックスは、pageIndex
だ。ここが、先に書いた、size
と連動している部分である。
MAX_NUMBER_OF_ENTRY_BY_QUERY
は、1ページで取得するポケモンの件数だ。
offset
(取得開始したい ID の直前の ID)を、ここでは、pageStartPoint
としている。
pageStartPoint
は、上の図でも解説した通り、
const pageStartPoint = pageIndex * MAX_NUMBER_OF_ENTRY_BY_QUERY
となる。
fetcher
/**
* @description useSWRInfinite の引数の fetcher
*/
const fetcher = async (
query: string,
{ offset: offsetVal, limit: limitVal }: PokemonGraphqlVariables
): Promise<Pokemon[]> => {
const response = await request<QueryResult>(
'https://beta.pokeapi.co/graphql/v1beta',
query,
{
offset: offsetVal,
limit: limitVal
}
)
return convertRawResultToPokemon(response)
}
先述の getKey
で返された値が渡される。
なお、uhyo 氏のサンプルでは、GraphQL のリクエストに、urql を使用していらした。
urql も SWR もキャッシュ機能をもつ。
データを二重にキャッシュする必要性はないため、GraphQL クライアントには、graphql-request を採用した。
そうすると、GraphQL のリクエストは、
const response = await request<QueryResult>(
'https://beta.pokeapi.co/graphql/v1beta',
query,
{
offset: offsetVal,
limit: limitVal
}
)
のようになる。
offset: offsetVal
が動的に変化していく部分だ。
そして、これで得られた response
は、型 QueryResult
だ。
この response
を型 QueryResult
から、型 Pokemon[]
に整形する。
これが、convertRawResultToPokemon(response)
の部分だ。
convertRawResultToPokemon
自体は uhyo 氏の元のコードと同じだ。
便宜のため、以下にも記載する。
`convertRawResultToPokemon` 全体のコードを click で表示(src/features/inifiniteLoading/useCases/formattedPokemonListQuery.ts)
import { type QueryResult } from '@/features/inifiniteLoading/customHooks/usePokemonListSWRInfinite'
/**
* @typedef response 整形後の型
*/
export interface Pokemon {
id: number
en: string
ja: string
}
/**
* @description response を整形
*/
export function convertRawResultToPokemon(response: QueryResult): Pokemon[] {
return response.species.map((species) => {
const en =
species.pokemon_v2_pokemonspeciesnames.find((n) => n.language_id === 9)
?.name ?? ''
const ja =
species.pokemon_v2_pokemonspeciesnames.find((n) => n.language_id === 11)
?.name ?? ''
return {
id: species.id,
en,
ja
}
})
}
ここで、fetcher
が返す、データは以下のようになる。
[
{
"id": 1,
"en": "Bulbasaur",
"ja": "フシギダネ"
},
{
"id": 2,
"en": "Ivysaur",
"ja": "フシギソウ"
},
{
"id": 3,
"en": "Venusaur",
"ja": "フシギバナ"
},
……(中略)
{
"id": 49,
"en": "Venomoth",
"ja": "モルフォン"
},
{
"id": 50,
"en": "Diglett",
"ja": "ディグダ"
}
],
// 1ページ目の配列 ↑↑
usePokemonListSWRInfinite
(useSWRInfinite
のカスタムフック)ポケモンデータを取得
/**
* @typedef usePokemonListSWRInfinite の戻り値の型
*/
interface UsePokemonListSWRInfiniteReturnType {
pokemonsDataArr: Array<readonly Pokemon[]>
error: Error | undefined
isLoading: boolean
loadMorePage: () => void
isReachingEnd: boolean
}
/**
* @description
* useSWRInfinite でポケモンデータを取得する
* ページサイズをインクリメントする関数を返す
* など。
*/
export const usePokemonListSWRInfinite =
(): UsePokemonListSWRInfiniteReturnType => {
const { data, error, isLoading, size, setSize } = useSWRInfinite<
Pokemon[],
Error
>(
getKey,
async ([query, pokemonGraphqlVariables]: ApiKeyType) =>
await fetcher(query, pokemonGraphqlVariables),
{ revalidateFirstPage: false }
)
// すべてのデータを取得したら、true
const isReachingEnd: boolean =
data !== undefined &&
data[data.length - 1]?.length < MAX_NUMBER_OF_ENTRY_BY_QUERY
const emptyData: Array<readonly Pokemon[]> = []
const pokemonsDataArr: Array<readonly Pokemon[]> =
typeof data === 'undefined' ? emptyData : data
// ページを増やす => 最新のページを読み込むtrigger
const loadMorePage = (): void => {
void setSize(size + 1)
}
return {
pokemonsDataArr,
error,
isLoading,
loadMorePage,
isReachingEnd
}
}
useSWRInfinite 部分
const { data, error, isLoading, size, setSize } = useSWRInfinite<
Pokemon[],
Error
>(
getKey,
async ([query, pokemonGraphqlVariables]: ApiKeyType) =>
await fetcher(query, pokemonGraphqlVariables),
{ revalidateFirstPage: false }
)
useSWRInfinite
のリクエスト部分。
useSWRInfinite<Pokemon[], Error>
とすることで、返り値の data
と error
に型の指定ができる。
この場合、data
の型は、Array<Pokemon[]> | undefined
に、
error
の型は、Error | undefined
になる。
async ([query, pokemonGraphqlVariables]: ApiKeyType) =>
の部分について。
引数が、複数のときは、このように配列で渡す。
(ドキュメント 複数の引数参照 https://swr.vercel.app/ja/docs/arguments#複数の引数 )
{ revalidateFirstPage: false }
は、「常に最初のページを再検証する」設定をオフにしている。
ここで、 data
の戻る。
data
は、ページごとの複数の API レスポンスの配列になる。
つまり、以下のような二重配列になる。
useSWRInfinite の返り値の data
[
// 1ページ目の配列 ↓↓
[
{
"id": 1,
"en": "Bulbasaur",
"ja": "フシギダネ"
},
{
"id": 2,
"en": "Ivysaur",
"ja": "フシギソウ"
},
{
"id": 3,
"en": "Venusaur",
"ja": "フシギバナ"
},
……(中略)
{
"id": 49,
"en": "Venomoth",
"ja": "モルフォン"
},
{
"id": 50,
"en": "Diglett",
"ja": "ディグダ"
}
],
// 1ページ目の配列 ↑↑
// 2ページ目の配列 ↓↓
[
{
"id": 51,
"en": "Dugtrio",
"ja": "ダグトリオ"
},
{
"id": 52,
"en": "Meowth",
"ja": "ニャース"
},
……(中略)
{
"id": 99,
"en": "Kingler",
"ja": "キングラー"
},
{
"id": 100,
"en": "Voltorb",
"ja": "ビリリダマ"
}
],
// 2ページ目の配列 ↑↑
// 3ページ目の配列 ↓↓
[
{
"id": 101,
"en": "Electrode",
"ja": "マルマイン"
},
{
"id": 102,
"en": "Exeggcute",
"ja": "タマタマ"
},
……(中略)
{
"id": 149,
"en": "Dragonite",
"ja": "カイリュー"
},
{
"id": 150,
"en": "Mewtwo",
"ja": "ミュウツー"
}
]
// 3ページ目の配列 ↑↑
]
// すべてのデータを取得したら、true
const isReachingEnd: boolean =
data !== undefined &&
data[data.length - 1]?.length < MAX_NUMBER_OF_ENTRY_BY_QUERY
const emptyData: Array<readonly Pokemon[]> = []
const pokemonsDataArr: Array<readonly Pokemon[]> =
typeof data === 'undefined' ? emptyData : data
// ページを増やす => 最新のページを読み込むtrigger
const loadMorePage = (): void => {
void setSize(size + 1)
}
-
isReachingEnd
: すべてのデータを取得したら、true
- すべてのデータを取得し終わったら、最後の API レスポンスの配列の要素数は、
MAX_NUMBER_OF_ENTRY_BY_QUERY
より小さくなるので、その条件で判定。
- すべてのデータを取得し終わったら、最後の API レスポンスの配列の要素数は、
-
pokemonsDataArr
: ポケモンデータ-
typeof data === 'undefined'
のときは、空の配列となるようする。
-
-
loadMorePage
: ページサイズをインクリメントする関数。- 後述する、Loading コンポーネント内で使用する。
以上で、usePokemonListSWRInfinite.ts
の説明を終える。
Loading コンポーネント
Loading コンポーネント から、usePokemonListSWRInfinite
までの処理の流れは、
ローディングが表示
=> フェッチして返されるであろうページをインクリメント
=> getKey
が返す、API のキーが変化 =>
=> 新規 API リクエスト
である。
前半の「ローディングが表示 => フェッチして返されるであろうページをインクリメント」が、Loading コンポーネントで行う。
全体のコードは以下。
import { type FC, useEffect, useRef } from 'react'
import { useIsIntersecting } from '@/features/inifiniteLoading/customHooks/useIsIntersecting'
import { usePokemonListSWRInfinite } from '@/features/inifiniteLoading/customHooks/usePokemonListSWRInfinite'
/**
* @description
* Loading component.
* viewport と交差したとき、ポケモンAPIを実行するtriggerとなる。
*/
export const Loading: FC = () => {
const observedRef = useRef<HTMLParagraphElement | null>(null)
const { loadMorePage, isReachingEnd } = usePokemonListSWRInfinite()
// トリガーが表示されているか監視
const isIntersection: boolean =
useIsIntersecting<HTMLParagraphElement>(observedRef)
useEffect(() => {
// トリガーが表示されたらデータを取得
if (isIntersection && !isReachingEnd) {
loadMorePage()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isIntersection, isReachingEnd])
return <p ref={observedRef}>Loading...</p>
}
大きな流れとしては、
ローディングが表示されるとき、isIntersection
が、true
になり、
isIntersection
に依存している、useEffect
で、loadMorePage()
が実行される。
isIntersection
の判定は、交差判定するカスタムフックを使用している。
交差判定カスタムフック
要素が表示されると、true
を返すカスタムフックは、りーほーブログさんの記事を参考にさせていただいた。
以下のように、引数に監視対象の要素を取る。
監視対象の要素が表示されると true
を返す。
独自の工夫としては、ジェネリクス(RefElement
)を使い、
監視対象の要素の型を、カスタムフックを呼び出すときに指定できるようした。
import { useEffect, useState } from 'react'
/**
* @description 要素に監視対象のDOMを取る。
* 監視対象のDOMが画面に表示されると、true を返す。
*/
export const useIsIntersecting = <RefElement extends HTMLElement>(
ref: React.MutableRefObject<RefElement | null>
): boolean => {
const [isIntersecting, setIsIntersecting] = useState(false)
useEffect(() => {
if (ref.current == null) return
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting)
},
{
rootMargin: '0px 0px 100px 0px'
}
)
let observerRefCurrent: RefElement | null = null
// 監視を開始
observer.observe(ref.current)
observerRefCurrent = ref.current
return () => {
// 要素の監視を終了する
observer.unobserve(observerRefCurrent as RefElement)
}
})
return isIntersecting
}
ポケモンデータ表示コンポーネント
ポケモンデータの表示のコンポーネントは以下のようになる。
基本的には、uhyo 氏の元記事のコードの流用だ。
変えた部分は、
- ロード表示とエラー表示を追加
- データの二重配列に対応 => map を二重にした
-
isReachingEnd
真偽値を使い。データをすべて取得し終わったあとに、メッセージを表示
などだ。
import { type FC, Fragment } from 'react'
import { Loading } from '@/features/inifiniteLoading/components/Loading'
import { usePokemonListSWRInfinite } from '@/features/inifiniteLoading/customHooks/usePokemonListSWRInfinite'
/**
* @description ポケモンのデータを表示する。
*/
export const PokemonList: FC = () => {
const { pokemonsDataArr, isLoading, error, isReachingEnd } =
usePokemonListSWRInfinite()
if (error != null) {
return <>Error: {error}</>
}
if (isLoading || pokemonsDataArr.length === 0) {
return <>loading……(ロード中)</>
}
return (
<Fragment>
<dl>
{pokemonsDataArr.map((pokemons) => {
return pokemons.map((pokemon) => (
<div key={pokemon.id}>
<dt lang="ja">{pokemon.ja}</dt>
<dd>
{pokemon.en} <span>#{pokemon.id}</span>
</dd>
</div>
))
})}
</dl>
{!isReachingEnd && <Loading />}
{isReachingEnd && (
<p style={{ margin: '10px 0 10px' }}>
All data loading is complete.
(すべてのデータの読み込みが完了しました。)
</p>
)}
</Fragment>
)
}
まとめ
ここでまとめを考えたが、記事の前半に記載した「要旨」以上のものが思い浮かばないため、再掲する。
- SWR には、無限ローディング(無限ローディング)を実現することを想定した API がある。
- それは、
useSWRInfinite
である。 - ある意味、無限ローディング専用の API なので
useSWRInfinite
を使うと、シンプルに無限ローディングが実装できる。 -
useSWRInfinite
では、GraphQL も利用可能。
- それは、
- 無限ローディングで、交差オブザーバー API (Intersection Observer API) を利用するときには、カスタムフックを作ると便利である。
- 具体的には、交差が起きると
true
を返すようなカスタムフックを作る。
- 具体的には、交差が起きると
- 無限ローディングを実装するには、交差オブザーバー API のカスタムフックが
true
を返したときに、React のuseEffect
を使って、useSWRInfinite
で新しいページのリクエストを走るようにすればよい。
感想
専用の API を使っているので、当然と言ったら当然なのだが、それほど難しいことをしなくても、無限ローディングが実現できたので満足している。
-
「Recoil selector 活用パターン 無限スクロール編」より引用 ↩︎
Discussion