React|Infinite Scrollの実装


無限スクロール


無限スクロールは、ユーザがページの下部に到達したときにコンテンツをロードし続けるユーザエクスペリエンス(UX)である.無限スクロールを実現するために必要な技術は次のとおりです.
  • コンテンツ取得API
  • ページ下部要素観察
  • 新規コンテンツを追加
  • インポートされたコンテンツのデータ取得は、キャッシュおよび最適化のためにreact-queryを使用して実施され、react-intersection-observerを使用してページの下部の要素を観察する.

    [useInfiniteQuery]


    React Queryは、応答からデータを取得、キャッシュ、更新するライブラリであるuseInfiniteQuery Hookを提供します.
    React Query公式ドキュメントによれば、次のパラメータと戻り値があります.
    const {
      fetchNextPage,
      fetchPreviousPage,
      hasNextPage,
      hasPreviousPage,
      isFetchingNextPage,
      isFetchingPreviousPage,
      ...result
    } = useInfiniteQuery(
        queryKey, 
        ({ pageParam = 1 }) => fetchPage(pageParam), 
        {
          ...options,
          getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
          getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
        }
    );

    パラメータ


    1.最初のパラメータ-queryKey


    react-queryは、データをキャッシュするためのkey値です.
    開発者は任意に指定できます.
    各ロードされたデータは、異なるkey値を使用する必要があります.

    2.2番目の引数-fetch関数

    ({ pageParam = 1 }) => fetchPage(pageParam)
    2番目の引数は、要求データを含むAPI関数の関数です.fetchPageは、応答の値を返さなければならない.
    無限スクロールを実行する必要がある場合は、次の要求データを含むデータを返すことができます.
    たとえば、現在1ページ目のデータを要求している場合は、結果値を戻り値とともにエクスポートして、次回2ページ目のデータを要求できます.pageParamは使用される値であり、初期ページを初期ページに設定することができる.
    要約すると、結果値とともにnextPageおよびisLastの情報を導出すれば、次の要求でそれらを使用することができる.
    const fetchPage = ({ pageParam = 0 }) => {
      // API
      const { data } = await getData({ startIndex: pageParam });
      
      // 다음 요청시 사용할 nextPage와 isLast
      return {
        result: data,
        nextPage: pageParam + 1,
        isLast: data.isLast,
      }
    }
    

    3.第3次買収-options

    {
      ...options,
        getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
    }
    最後のパラメータはオプションとして他の機能を提供します.
    さらにデータを取得する場合、getNextPAgeParamは、第2のパラメータコールバック関数が返す値を取得するために使用することができる.したがって、上記の例で返されるオブジェクトはnextPageを含み、lastPage.nextPageとして使用することができる.getNextPageParamは、2番目の引数コールバック関数が呼び出されたときに引数として渡される1つの値のみを返さなければならない.undefinedを返すとfetchコールバックは呼び出されませんので、最後のpayであれば利用できます.

    戻り値


    1.Next Pageコールバックの取得


    この関数を呼び出すと、次のページからデータを要求できます.この場合、2番目の引数に渡されるコールバックは、次のページ情報をパラメータとして受信し、呼び出す.

    2. isFetching

    useQueryisLoadingとは異なり、isFetchingでロードされていると判断できます.

    3. data

    dataは、各ページのデータをリスト形式でリストする.ページ固有のデータには、2番目のパラメータコールバックの戻り値が含まれます.
    上記コールバックが使用されている場合、dataは以下のようになります.
    const data = {
      pages: [
        {
          result,
          nextPage,
          isLast,
        },
        {
          result,
          nextPage,
          isLast,
        },
        // ...
        ]
    }

    [useInView]


    Intersection Observer APIは、デフォルトのJS仕様に含まれるAPIです.React Intersection Observerライブラリを使用して、リトラクト中にIntersection Observer APIを使用します.
    ライブラリドキュメントuseInView Hookを提供し、以下に示す.
    import React from 'react';
    import { useInView } from 'react-intersection-observer';
    
    const Component = () => {
      const [ref, inView] = useInView();
    
      return (
        <div ref={ref}>
          <h2>{`Header inside viewport ${inView}.`}</h2>
        </div>
      );
    };

    戻り値


    1. ref


    refを観察する要素に渡せばよい.

    2. inView


    boolean値.ビューポートに観察中の要素が表示されている場合はtrue、さらにfalseを返します.

    [無限スクロールを実現]


    上記の2つの技術を用いて無限スクロールを実現した.
    戻り値で観察する要素(ObservationComponent)をコンテンツの一番下に追加すると、正常に動作します.ObservationComponentが観察されると、inViewの値が変更されるので、useEffectを使用してfetch関数が呼び出されます.
    import { ReactElement, useEffect } from 'react';
    import { useInView } from 'react-intersection-observer';
    import { useInfiniteQuery } from 'react-query';
    
    import { AnyObject } from 'immer/dist/internal';
    
    import { DEFAULT_SEARCH_QUANTITY, SearchType } from '@type/web/event';
    
    import { getEventList } from '@api/event/event';
    
    interface UseInfiniteQueryWithScrollParamsTypes {
      currentSearchType: SearchType;
      queryString: AnyObject;
    }
    
    interface UseInfiniteQueryWithScrollReturnTypes {
      data: any[] | undefined;
      error: string | undefined | unknown;
      isFetching: boolean;
      ObservationComponent: () => ReactElement;
    }
    
    /**
     * 사용 기술
     * Recat query: useInfiniteQuery (https://react-query.tanstack.com/guides/infinite-queries)
     * react-intersection-observer: useInView
     */
    export default function useInfiniteQueryWithScroll({
      currentSearchType,
      queryString,
    }: UseInfiniteQueryWithScrollParamsTypes): UseInfiniteQueryWithScrollReturnTypes {
      const getEventListWithPageInfo = async ({ pageParam = 0 }) => {
        const { data } = await getEventList({
          ...queryString,
          eventType: currentSearchType,
          searchType: currentSearchType,
          startIndex: pageParam,
        });
    
        const nextPage =
          data.length >= DEFAULT_SEARCH_QUANTITY ? pageParam + 1 : undefined;
    
        return {
          result: data,
          nextPage,
          isLast: !nextPage,
        };
      };
    
      const { data, error, isFetching, fetchNextPage } = useInfiniteQuery(
        [`eventListData-${currentSearchType}`],
        getEventListWithPageInfo,
        {
          getNextPageParam: (lastPage) => lastPage.nextPage,
        },
      );
    
      const ObservationComponent = (): ReactElement => {
        const [ref, inView] = useInView();
    
        useEffect(() => {
          if (!data) return;
    
          const pageLastIdx = data.pages.length - 1;
          const isLast = data?.pages[pageLastIdx].isLast;
    
          if (!isLast && inView) fetchNextPage();
        }, [inView]);
    
        return <div ref={ref} />;
      };
    
      return {
        data: data?.pages,
        error,
        isFetching,
        ObservationComponent,
      };
    }