https://blog.ojisan.io/i-hate-infinite-scroll/

visual.jpg

blog.ojisan.io

毎年無限スクロールの実装をしているのだが正直なところ実装したくないので依頼されたときの反論材料として実装したくない理由を言語化しておこうと思う。

無限スクロールが何を指すかを知らない人のために解説すると、ページにコンテンツを足す方式でページネーションする UI を指している。例えば Twitter のように下にどんどんコンテンツが伸びていく UI が良い例だろう。そのような UI を無限スクロールと呼ぶことが正式なのかは知らないが、このような体験の実現を支援するライブラリに infinite-scroll というものがあり、少しは普及している呼び方なのだと思い無限スクロールという言葉を使う。一方で WEB フロントエンド文脈で無限スクロールと言うと複雑 GUI やドローイングツール実装における "無限平面" のようなニュアンスもあるが、今は無限平面のことを指しているわけではない。

NextJS を例に解説するが、React であれば react-router でも同様のことはできるはずである。react 以外の例は試していないが考えることは同じであるはず。

また、まずは愚直にやると辛い例を示したいので、雑な実装を紹介する。正答は後の方で解説する。

無限スクロールなんていう仰々しい名前だが、結局はページネーションの一つの形態であるのでバックエンドの API はいわゆる普通のページネーションである。つまり取得量である volume は一定として offset や cursor のようなものを指定するエンドポイントがあれば良い。ここでは offset を指定したら offset 番目以降のデータが一定 volume で取得できる API があると仮定する。この方式が気になる方は "Offset-based pagination" や "Cursor-based pagination" などで検索すると良いだろう。

ここではコンテンツは「もっと」ボタンをクリックして増えるものとする。

<button onClick={onClick}>もっと</button>

onClick などのロジックは offset 操作用に定義した hooks で管理しておく。

const useGetMore = () => {
  const [data, setData] = useState([]);
  const [offset, setOffset] = useState(0);

  useEffect(() => {
    fetch(`/api/data?offset=${offset}`)
      .then((res) => res.json)
      .then((data) => {
        setData(data);
      });
  }, [offset]);

  const onClick = useCallback(() => {
    setOffset(offset + DEFAULT_VOLUME);
  }, [offset]);

  return { onClick, data };
};
const Hoge = () => {
  const { data, onClick } = useGetMore();
  return (
    <div>
      <Contents data={data} />
      <button onClick={onClick}>もっと</button>
    </div>
  );
};

とりあえずこれで「もっと」を押すとデータが更新されることとはなった。 しかしお気づきの通りこの実装は穴がたくさんある。 それを今から見ていこう。

これから無限スクロールの考慮しないといけない点を紹介する。 また、実装経験がそれなりにあるのでこれらの解決策は知っているので解決策も紹介する。 ここで解決策を書くと「え、解決できるなら実装してよ」だったり「君は慣れてるからこの仕事よろしくね」と言われそうなので書くか悩んだのだが、いま辛い目にあっている人たちを見捨てるわけにはいかないので書いておこうと思う。

さて、先ほどの例の

const Hoge = () => {
  const { data, onClick } = useGetMore();
  return (
    <div>
      <Contents data={data} />
      <button onClick={onClick}>もっと</button>
    </div>
  );
};

は、追加コンテンツを得ようとすると <Contents data={data} /> 全体に更新が走ってしまう。 それをもっとわかりやすくインターフェースを変えて、