Zenn
📖

pagination(無限スクロール)の実装

2025/02/22に公開

使用技術

front

  • nextjs
  • chakraui
  • tanstackquery

back

  • go

概要

スクロールしてスクロールの一番下までいくと追加で読み込んでくれる無限スクロールを実装する
一度に大量の取得を行わないので時間がかからずuxが良い

リクエスト

{
    limit // 一度に取得するデータ数
    after // 次回どこから取得するか(idなど)
}

レスポンス

{
    list:[data1, data2...]
    pagination: {
        nextCursor: 'afterが入る'
    }
}

frontの実装

  • クエリ
    initialPageParamが初めのafterになる
    その後getNextPageParamによって返ってきたnextCursorがafterとして次にリクエストされる
import { useInfiniteQuery } from '@tanstack/react-query'

export const usePaginatedData = (
  limit = 30 //デフォルト
) => {
  return useInfiniteQuery({
    queryKey: ['pagingData', limit],
    queryFn: ({ pageParam }) =>
      api.List({
        after: pageParam ?? undefined,
        limit: limit,
      }),
    initialPageParam: undefined,
    getNextPageParam: (lastPage: PaginatedData) => {
      return lastPage.pagination?.nextCursor || undefined
    },
  })
}
  • 機構
    useEffectでobserverからスクロールを監視
    スクロールが一番下までいくとfetchNextPageを発火
const {
    data: paginatedData,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = usePaginatedData()

---
// 以下の形でpropsとして渡した
pagination={
        fetchNextPage,
        hasNextPage,
        isFetchingNextPage
    }
// スクロールの監視
const loadMoreRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    if (!pagination) {
      return
    }

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && pagination.hasNextPage) {
          pagination.fetchNextPage()
        }
      },
      {
        root: containerRef.current,
        threshold: 0.1,
      }
    )

    const ref = loadMoreRef.current
    if (ref) {
      observer.observe(ref)
    }

    return () => {
      if (ref) {
        observer.unobserve(ref)
      }
    }
  }, [pagination])
  • 表示
    paginationでバラバラのデータを結合して渡す
    スクロールの最後に目印としてrefを追加する
// データの結合
const data = useMemo(() => {
    return paginatedData?.pages.flatMap((page) => page.manualCosts) || []
}, [paginatedData])

<VStack>
    data.map((item, idx) => {
        // 各itemの表示
    })
    <div ref={loadMoreRef}>{pagination?.isFetchingNextPage ? <Spinner /> : null}
</VStack>

backendの実装(usecase層のみ)

エラー処理などは省略している
一つ多く取ってきてnextCursorを定めている

func (u *usecase) List(ctx context.Context after *uuid.UUID limit int) (*ListData, err) {
    queryLimit := limit + 1

    var items []model.Item
    var err error

    if after != nil {
        baseItem, err := datastore.get(ctx, *after) // 基準となるitemを取得
        // 基準以降のデータを取得(順番はソートによる)
        items, err = datastore.list(ctx, queryLimit, where(sortField <= baseItem.sortField))
    } else {
        items, err = datastore.list(ctx, queryLimit)
    }

    // 1つ多く取ってきてるのでそこからnextCursorを定め, 本来の数のデータを返す
    var nextCursor *uuid.UUID
    if len(items) == queryLimit {
        nextCursor = &items[limit].ID
        items = items[limit]
    }

    return &listData{
        list: items,
        pagination: {
            nextCursor: nextCursor
        }
    }, nil
}

Discussion

ログインするとコメントできます