2021年1月22日金曜日

Reactで綺麗に無限スクロールを実現するためにIntersectionObserverを使いましょう的なお話

  • このエントリーをはてなブックマークに追加

色々と実装していると、なんだかんだで無限スクロールを実装したいことって多々あるわけで。
もちろん単純なページングを実装してもいいのだけど、React使ってSPA的なことを書いているとページングよりも無限スクロールで配列にガンガン後ろに足していくっていう方が楽だったりもするわけで。
その際にもっと見るボタンをユーザが押して新しくデータを取って来て云々をしてもいいけどやっぱ流れるようにみたいわけで。

ってことでIntersectionObserverを使うとそこらへんの実装が簡単になるので、それをhooks化して使っちゃおう的なお話をば。

ちなみに通常実装するならば、window.innerHeightとかscrollTopとかwindowにscrollイベント付与してとか面倒なことがあったり、というかそもそもReactなんだしDOMを直接見るとかそういうことはしたくないわけで。
だからIntersectionObserverを使いましょう的な話だったりする。
あとコンポーネントでwindowにscrollイベント付与するといくつも付与される可能性があってあまりよくないよね的な。

// hooks/intersection.ts
import { useState, useEffect } from 'react';

export const useIntersection = (ref: React.MutableRefObject<HTMLDivElement>) => {
  const [intersecting, setIntersecting] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIntersecting(entry.isIntersecting);
    });

    observer.observe(ref.current);

    return () => {
      observer.unobserve(ref.current);
    };
  });

  return intersecting;
};

こういうhooksを作ってあげる。
IntersectionObserverは簡単にいうとブラウザで表示されているかどうかを判別してコールバックを走らせることができる。
なのでそのタイミングでuseStateでboolean値を変更してあげるようなhooksを書いてあげれば画面に表示されたっていうのがわかるようになる的な。

// app.tsx
import React, { useEffect, useRef, useState } from 'react';
import { useIntersection } from './hooks/intersection';

export default function App(props: Props) {
  const ref = useRef<HTMLDivElement>(null) as React.MutableRefObject<HTMLDivElement>;
  const intersection = useIntersection(ref);
  const [intersected, setIntersected] = useState<boolean>(true);
  const [fetching, setFetching] = useState<boolean>(true);
  const [items,setItems] = useState<any[]>([]);
  
  const onButtonClick = async () => {
    setFetching(true);
    
    // データ取得処理
    const response = await ...
    setItems(response);
    
    setFetching(false);
  }
  
  useEffect(() => {
    setIntersected(intersection);
  }, [intersection]);
  
  useEffect(() => {
    if(intersected && !fetching) {
      onButtonClick();
    }
  }, [intersected,fetching]);
  
  return (
    ...
    
    <div className="items">
      {items.map((item) => <div className="item">...</div>)}
    </div>
    
    {!fetching && (
      <div ref={ref}>
        <button onClick={onButtonClick}>もっと見る</button>
      </div>
    )}
  )
}

ってな感じ。
割と丁寧に書いたらこうなるのかなと。
もっと見るボタンが表示されると発火してしまうので、基本的にはデータを取得している最中は隠してあげるとよい。
まぁぶっちゃけもうちょっとちゃんとhooks書けばもっと綺麗にかけるんだけども面倒だから書かなかったという。
ちなみに無限スクロールではなく、スクロールしてヘッダーの背景色が変わるとかだったらもっと簡素に実現できる。


ちょっとこの一年くらい、記事を書くとかLTをするとか表に出るような活動を全然できていなかった。
もちろんコロナという情勢がというのもあるし、仕事が忙しかったからという言い訳もできたりはするけど、多分単純な怠慢から来たもの。

なので今年は気持ち新たにまた真面目に記事を書いたりLTをする機会を作ったりしていこうかな的なみたいな。

0 件のコメント:

コメントを投稿

Adsense