本記事について
Reactの標準のやつだけで無限スクロールを実装します。
ググるとreact-infinite-scrollerを使う記事が多く見つかりますし、他にも無限スクロールを実現するいくつかのライブラリが存在します。
しかしながら、相性の問題や各種制約のためライブラリを使う選択肢がない場合もあると思うのです。
そうした方の力になれたらいいな、と思い、記事を公開するに至りました。
ただし、本記事では両方向への無限スクロールは対応していません。
また、画面を埋めるのに十分な数の要素を初期表示する必要があります。(=InfiniteScroll.jsのonStart()関数のaddItem()の引数の数字を十分大きくすること)
とりあえずInfiniteScroll.jsとIntersection.jsをコピペしてレンダーすれば、CSSが効かない等の特殊な状況でもない限りお試しいただけるかと思います。
何のコードかの説明や、解説は記事の後の方にあります。
コード
無限スクロール・コンポーネント
// app.tsx
import React, { useEffect, useRef, useState } from 'react';
import useIntersection from './Intersection';
export default function InfiniteScroll() {
const [dontchange, setDontchange] = useState();
const [fetching, setFetching] = useState(true);
const refNext = useRef(null);
const intersectionNext = useIntersection(refNext);
const [intersectedNext, setIntersectedNext] = useState(true);
const [items, setItems] = useState([]);
const [lastItemId, setLastItemId] = useState(0);
const addItem = (count) => {
const elems = [];
for (let i=0; i<count; i++){
const itemId = lastItemId + i;
const elem = <div key={"item_" + itemId}>
<p>Hello world.</p>
</div>
;
elems.push(elem);
}
setLastItemId(lastItemId + count);
setItems([...items, elems]);
}
const onStart = async () => {
//コンポーネント生成時に行う処理
//TODO: 初期表示する要素などがあればここでロードする
addItem(30);
setFetching(false);
}
useEffect(() => {
onStart();
}, []);
const onIntersectNext = async () => {
setFetching(true);
// TODO: ここで要素を追加する
addItem(5);
setFetching(false);
}
useEffect(() => {
setIntersectedNext(intersectionNext);
}, [intersectionNext]);
useEffect(() => {
if(intersectedNext && !fetching) {
onIntersectNext();
}
}, [intersectedNext,fetching]);
const loadingText = (
<p>Loading...</p>
);
return (
<>
<div
padding="10px 10px 10px 10px"
style={{
overflowX:"hidden"
,overflowY:"scroll"
,display: "flex"
,flexDirection: "column"
,position: "relative"
,alignItems: "center"
}}
>
<div
style={{
}}
>
{items}
{!fetching ? (
<div ref={refNext}>
{loadingText}
</div>
) : (
<>{loadingText}</>
)}
</div>
</div>
</>
)
}
import { useState, useEffect } from 'react';
const useIntersection = (ref) => {
const [intersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setIntersecting(entry.isIntersecting);
});
if (ref.current == null) return;
observer.observe(ref.current);
return () => {
if (ref.current == null) return;
observer.unobserve(ref.current);
};
});
return intersecting;
};
export default useIntersection;
その他のコード(無限スクロール以外の部分)
以下は無限スクロールとは関係ありませんが、とりあえず動かしたい、といった方向けです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import InfiniteScroll from './InfiniteScroll';
function App() {
return (
<div className="App">
<InfiniteScroll></InfiniteScroll>
</div>
);
}
export default App;
解説
原理
やっていること
- 一番下までスクロールした時に表示される要素をページの一番下に表示しておく
- この要素が画面内に入ったことを検出するコールバックを実装することで、一番下までスクロールしたことを検知する
- これをトリガーにして要素を追加する
- 要素が追加されることで1.の要素が隠れるので、次のスクロールができるようになる
解説
-
Intersection.js
7行目でIntersectionObserver
を生成- これは「交差オブザーバーAPI」というJavaScriptの標準のやつ
https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API - 画面領域(ビューポート)と当該要素が交差したときにコールバックすることができる。
- これは「交差オブザーバーAPI」というJavaScriptの標準のやつ
const observer = new IntersectionObserver();
- ReactのrefをIntersectionObserverに登録
- 1.で生成した
IntersectionObserver
オブジェクトに対してobserve()
メソッドを呼び出す。 - ここで
ref
を渡すことで、交差オブザーバーAPIの監視対象にReact要素を追加することができる。 - 監視対象に追加された要素は、要素が移動したり画面がスクロールされる等の要因により画面領域(ビューポート)に「交差していない状態」から「交差している状態」へ変化したときにコールバックされる
- 1.で生成した
const refNext = useRef(null);
//(中略)
{!fetching ? (
<div ref={refNext}>
{loadingText}
</div>
) : (
<>{loadingText}</>
)}
observer.observe(ref.current);
- IntersectionObserverのコールバックでstateを更新
- コールバックされたら、その旨を
InfiniteScroll
コンポーネントに伝えるためにstateの値を変更する。 - stateへの参照はあらかじめ
InfiniteScroll
コンポーネントに渡しておく。
本記事ではuseIntersection()
の戻り値でこのstateを返しており、InfiniteScroll
内のuseEffect()
でstateが変更された時の処理を記述している。
- コールバックされたら、その旨を
const [intersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setIntersecting(entry.isIntersecting);
});
//(中略)
});
return intersecting;
const intersectionNext = useIntersection(refNext);
const [intersectedNext, setIntersectedNext] = useState(true);
- stateが更新されたら、要素を追加する
-
useEffect()
でstateが変更されたことを検出して、要素の追加を実行する。
-
useEffect(() => {
if(intersectedNext && !fetching) {
onIntersectNext();
}
}, [intersectedNext,fetching]);
const onIntersectNext = async () => {
setFetching(true);
// TODO: ここで要素を追加する
addItem(5);
setFetching(false);
}
結果
このコンポーネントを表示すると「Hello world.」が縦に並んでたくさん表示される。
スクロールしてIntersectionObserverに登録した要素が画面内に入ったことをトリガーに「Hello world.」の表示が追加される。
IntersectionObserverに登録した要素は追加された「Hello world.」要素が挿入されたことで画面外に追いやられ、画面領域(ビューポート)との交差状態が解除される。
この繰り返しにより、一番下までスクロールするたびに要素が追加され、無限スクロールが成立する。
追加する要素を生成している箇所をバックエンド呼び出しに変えると、本記事の例よりも要素の追加が遅くなる。
そうすると「Loading...」が表示される時間が長くなり、それっぽい見た目になるのではないだろうか。
問題点
逆方向の無限スクロールができていない。
「Hello world.」要素を追加してもIntersection用要素が画面外に移動しないのが要因である。
対応として、要素がついかされた分だけ自動でスクロールする、といったことが考えられるが、
HTML要素(またはReact要素)の縦サイズは一度レンダーしないと決まらないので取得できず、スクロール量を知り得ないので実現しないのではないだろうか。
と思う。妙案があれば・・