Infinite Scroll

PreviousNext

An infinite scroll component for react native

Docs
rigiduicomponent

Preview

Loading preview…
r/new-york/infinite-scroll/infinite-scroll-rn.tsx
import React, { useCallback, useRef, useState } from 'react'
import { FlatList, View, Text, ActivityIndicator } from 'react-native'
import { cn } from '@/lib/utils'

const DefaultLoader = () => (
  <View className="flex justify-center py-4">
    <ActivityIndicator size="small" />
  </View>
)

const DefaultEndMessage = () => (
  <View className="text-center py-4">
    <Text className="text-muted-foreground">No more items to load</Text>
  </View>
)

interface InfiniteScrollProps<T> {
  items: T[]
  hasNextPage: boolean
  isLoading: boolean
  onLoadMore: () => void | Promise<void>
  threshold?: number
  loader?: React.ComponentType
  endMessage?: React.ReactNode
  errorMessage?: React.ReactNode
  renderItem: (item: T, index?: number) => React.ReactNode
  className?: string
  itemClassName?: string
  reverse?: boolean
  initialLoad?: boolean
  estimateSize?: number
  keyExtractor?: (item: T, index: number) => string
}

export function InfiniteScroll<T>({
  items,
  hasNextPage,
  isLoading,
  onLoadMore,
  threshold = 0.5,
  loader: Loader = DefaultLoader,
  endMessage = <DefaultEndMessage />,
  errorMessage,
  renderItem,
  className,
  itemClassName,
  reverse = false,
  initialLoad = false,
  estimateSize = 50,
  keyExtractor,
}: InfiniteScrollProps<T>) {
  const [internalLoading, setInternalLoading] = useState(false)
  const flatListRef = useRef<FlatList>(null)

  const handleLoadMore = useCallback(() => {
    if (internalLoading || !hasNextPage || isLoading) return

    setInternalLoading(true)
    Promise.resolve(onLoadMore()).finally(() => {
      setInternalLoading(false)
    })
  }, [hasNextPage, isLoading, internalLoading, onLoadMore])

  const defaultKeyExtractor = useCallback(
    (_item: T, index: number) => index.toString(),
    []
  )

  const renderItemWrapper = useCallback(
    ({ item, index }: { item: T; index: number }) => (
      <View className={cn(itemClassName)}>
        {renderItem(item, index)}
      </View>
    ),
    [renderItem, itemClassName]
  )

  const renderFooter = useCallback(() => {
    if (isLoading || internalLoading) {
      return <Loader />
    }

    if (!hasNextPage && items.length > 0) {
      return <>{endMessage}</>
    }

    return null
  }, [isLoading, internalLoading, hasNextPage, items.length, endMessage, Loader])

  const renderEmpty = useCallback(() => {
    if (isLoading || internalLoading) {
      return null
    }

    return (
      <View className="flex justify-center items-center py-8">
        <Text className="text-muted-foreground">No items found</Text>
      </View>
    )
  }, [isLoading, internalLoading])

  return (
    <View className={cn("flex-1", className)}>
      <FlatList
        ref={flatListRef}
        data={items}
        renderItem={renderItemWrapper}
        keyExtractor={keyExtractor || defaultKeyExtractor}
        onEndReached={handleLoadMore}
        onEndReachedThreshold={threshold}
        ListFooterComponent={renderFooter}
        ListEmptyComponent={renderEmpty}
        inverted={reverse}
        getItemLayout={
          estimateSize
            ? (_data, index) => ({
              length: estimateSize,
              offset: estimateSize * index,
              index,
            })
            : undefined
        }
        initialNumToRender={10}
        maxToRenderPerBatch={10}
        windowSize={5}
        removeClippedSubviews={true}
        contentContainerStyle={
          items.length === 0 ? { flexGrow: 1 } : { paddingBottom: 16 }
        }
      />
      {errorMessage && (
        <View className="text-center py-4">
          <Text className="text-destructive">{errorMessage}</Text>
        </View>
      )}
    </View>
  )
}

export type { InfiniteScrollProps }

Installation

npx shadcn@latest add @rigidui/infinite-scroll-rn

Usage

import { InfiniteScrollRn } from "@/components/infinite-scroll-rn"
<InfiniteScrollRn />