digitalfabrik/integreat-app

View on GitHub
web/src/components/InfiniteScrollList.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
F
0%
import React, { ReactElement, ReactNode, useCallback, useEffect, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroller'
import styled from 'styled-components'

import { loadAsync } from 'shared/api'

import FailureSwitcher from './FailureSwitcher'

const NoItemsMessage = styled.div`
  padding-top: 25px;
  text-align: center;
`

const StyledList = styled.div`
  position: relative;
  margin-bottom: 40px;
  padding-top: 1px;
`

type InfiniteScrollListProps<T> = {
  loadPage: (page: number) => Promise<T[]>
  noItemsMessage: string
  renderItem: (item: T) => ReactNode
  defaultPage: number
  itemsPerPage: number
}

const InfiniteScrollList = <T,>({
  loadPage,
  noItemsMessage,
  renderItem,
  defaultPage,
  itemsPerPage,
}: InfiniteScrollListProps<T>): ReactElement => {
  const [data, setData] = useState<T[]>([])
  const [error, setError] = useState<Error | null>(null)
  const [loading, setLoading] = useState<boolean>(false)
  const [page, setPage] = useState<number>(defaultPage)
  const [hasMore, setHasMore] = useState<boolean>(true)

  const load = useCallback(async () => {
    if (hasMore) {
      setLoading(true)
      setPage(page + 1)
      const request = () => loadPage(page)
      const addData = (data: T[] | null) => {
        if (data !== null) {
          setData(oldData => (page === defaultPage ? data : oldData.concat(data)))
          if (data.length !== itemsPerPage) {
            setHasMore(false)
          }
        }
      }
      await loadAsync(request, addData, setError, setLoading)
    }
  }, [defaultPage, page, hasMore, itemsPerPage, loadPage])

  useEffect(
    () => () => {
      setData([])
      setError(null)
      setLoading(false)
      setHasMore(true)
      setPage(defaultPage)
    },
    [loadPage, defaultPage],
  )

  if (error) {
    return <FailureSwitcher error={error} />
  }

  if (data.length === 0 && !hasMore) {
    return <NoItemsMessage>{noItemsMessage}</NoItemsMessage>
  }

  return (
    <StyledList>
      <InfiniteScroll loadMore={load} hasMore={!loading && hasMore}>
        <div>{data.map(renderItem)}</div>
      </InfiniteScroll>
    </StyledList>
  )
}

export default InfiniteScrollList