npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

paginated-flatlist

v1.0.2

Published

A high-performance, reusable paginated list component for React Native with infinite scroll, pull-to-refresh, and skeleton loading

Downloads

258

Readme

paginated-flatlist

A high-performance, feature-rich paginated list component for React Native built with TypeScript. Supports infinite scroll, pull-to-refresh, error handling, and dynamic filtering out of the box.

Perfect for building lists of restaurants, dishes, products, or any dynamic data that needs pagination.

✨ Features

  • ⚡ High Performance - Built on Shopify's FlashList for optimal rendering
  • ♾️ Infinite Scroll - Automatically load more items as users scroll to the end
  • 🔄 Pull-to-Refresh - Native pull-to-refresh gesture support with loading states
  • 📱 External Refresh Control - Use your own header/tab refresh button instead of pull-to-refresh
  • 🎨 Fully Customizable - Flexible rendering for headers, empty states, footers, and items
  • ⚙️ Automatic State Management - Handles loading, error states, pagination automatically
  • 🔍 Filter-Ready - Designed to work seamlessly with dynamic filters and search
  • 📦 TypeScript First - Fully typed for better developer experience
  • 🚀 Zero Custom Dependencies - Only essential peer dependencies required
  • ♿ Accessibility Built-in - Proper loading indicators and error messages

📦 Installation

npm install paginated-flatlist react-native @shopify/flash-list

or with yarn:

yarn add paginated-flatlist react-native @shopify/flash-list

or with pnpm:

pnpm add paginated-flatlist react-native @shopify/flash-list

Requirements

  • React Native >= 0.81.0
  • React >= 18.2.0 or ^19.0.0
  • @shopify/flash-list >= 2.1.0

🚀 Quick Start

Basic Usage

import React from 'react'
import { Text, View } from 'react-native'
import { PaginatedList } from 'paginated-flatlist'

interface User {
  id: string
  name: string
  email: string
}

export default function UserListScreen() {
  const fetchUsers = async (page: number, pageSize: number): Promise<User[]> => {
    const response = await fetch(
      `https://api.example.com/users?page=${page}&limit=${pageSize}`
    )
    return response.json()
  }

  return (
    <PaginatedList<User>
      fetchData={fetchUsers}
      pageSize={20}
      renderItem={({ item }) => (
        <View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' }}>
          <Text style={{ fontSize: 16, fontWeight: '600' }}>{item.name}</Text>
          <Text style={{ fontSize: 14, color: '#666' }}>{item.email}</Text>
        </View>
      )}
      keyExtractor={(item) => item.id}
    />
  )
}

📚 Real-World Example: Restaurant Dishes List

This example demonstrates the typical use case - filtering dishes by restaurant with tag-based filtering:

import React, { useCallback, useEffect, useRef, useState } from 'react'
import { View, Text, Pressable } from 'react-native'
import { PaginatedList, type PaginatedListRef } from 'paginated-flatlist'

interface Dish {
  _id: string
  name: string
  description: string
  price: number
  imageUrl: string
}

interface FilterParams {
  kioskId: string
  tags: string[]
  search: string
}

export default function KioskDishesScreen({ kioskId }: { kioskId: string }) {
  const [filters, setFilters] = useState<FilterParams>({
    kioskId,
    tags: [],
    search: '',
  })

  // Ref to control list from external refresh button
  const paginatedListRef = useRef<PaginatedListRef>(null)

  // Fetch function with current filters
  const fetchDishes = useCallback(
    async (page: number, pageSize: number): Promise<Dish[]> => {
      const query = new URLSearchParams({
        page: page.toString(),
        limit: pageSize.toString(),
        kioskId: filters.kioskId,
        tags: filters.tags.join(','),
        search: filters.search,
      })

      const response = await fetch(`https://api.example.com/dishes?${query}`)
      if (!response.ok) throw new Error('Failed to fetch dishes')
      return response.json()
    },
    [filters],
  )

  // Refresh list when filters change
  useEffect(() => {
    paginatedListRef.current?.handleRefresh()
  }, [filters])

  const handleExternalRefresh = async () => {
    try {
      await paginatedListRef.current?.handleRefresh()
    } catch (error) {
      console.error('Refresh failed:', error)
    }
  }

  const handleTagFilter = (selectedTagIds: string[]) => {
    setFilters((prev) => ({
      ...prev,
      tags: selectedTagIds,
    }))
  }

  return (
    <View style={{ flex: 1 }}>
      {/* Custom header with refresh button */}
      <View style={{ padding: 16, backgroundColor: '#f5f5f5' }}>
        <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Dishes</Text>
        <Pressable onPress={handleExternalRefresh}>
          <Text style={{ color: '#007AFF', marginTop: 8 }}>Refresh</Text>
        </Pressable>
      </View>

      {/* Tag filters */}
      <View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
        <TagFilterComponent onSelect={handleTagFilter} />
      </View>

      {/* Paginated list with external refresh control */}
      <PaginatedList<Dish>
        ref={paginatedListRef}
        fetchData={fetchDishes}
        pageSize={10}
        keyExtractor={(item) => item._id}
        renderItem={({ item }) => (
          <DishCard dish={item} />
        )}
        useExternalRefreshControl={true}
        ListEmptyComponent={
          <View style={{ padding: 20, alignItems: 'center' }}>
            <Text style={{ color: '#999' }}>No dishes found</Text>
          </View>
        }
      />
    </View>
  )
}

📖 API Reference

Props

| Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | fetchData | (page: number, pageSize: number) => Promise<T[]> | - | ✅ | Function to fetch paginated data. Should return an array of items. | | renderItem | ({ item, index }) => JSX.Element | - | ✅ | Render function for each list item | | pageSize | number | 10 | ❌ | Number of items per page/request | | keyExtractor | (item: T, index: number) => string | Uses array index | ❌ | Function to extract unique key for each item (improves performance) | | ListHeaderComponent | JSX.Element | undefined | ❌ | Component rendered at the top of the list (above all items) | | ListEmptyComponent | JSX.Element | Default empty message | ❌ | Component shown when list is empty | | ListFooterComponent | JSX.Element | Loading indicator | ❌ | Component shown at bottom of list | | horizontal | boolean | false | ❌ | Enable horizontal scrolling instead of vertical | | onRefresh | () => void \| Promise<void> | undefined | ❌ | Callback triggered when user pulls to refresh (before fetchData) | | useExternalRefreshControl | boolean | false | ❌ | When true, disables built-in pull-to-refresh and lets parent handle refresh |

Ref Methods

Access advanced functionality using useRef<PaginatedListRef>():

const listRef = useRef<PaginatedListRef>(null)

// Programmatically refresh the entire list (resets to page 1)
await listRef.current?.handleRefresh()

// Check if list is currently refreshing
const isRefreshing = listRef.current?.refreshing

🎯 Common Use Cases

1. Simple Product List

<PaginatedList<Product>
  fetchData={async (page, pageSize) => {
    const res = await fetch(`/api/products?page=${page}&limit=${pageSize}`)
    return res.json()
  }}
  renderItem={({ item }) => <ProductCard product={item} />}
  pageSize={15}
/>

2. With Header and Custom Empty State

<PaginatedList<User>
  fetchData={fetchUsers}
  renderItem={renderUserItem}
  ListHeaderComponent={
    <View style={{ padding: 16, backgroundColor: '#f0f0f0' }}>
      <Text style={{ fontSize: 20, fontWeight: 'bold' }}>Users</Text>
      <Text style={{ color: '#666' }}>Total: 1,234 users</Text>
    </View>
  }
  ListEmptyComponent={
    <View style={{ padding: 40, alignItems: 'center' }}>
      <Text style={{ fontSize: 16, color: '#999' }}>
        No users found. Try adjusting your filters.
      </Text>
    </View>
  }
/>

3. With Filters and Dynamic Updates

const [filters, setFilters] = useState({ category: '', search: '' })
const listRef = useRef<PaginatedListRef>(null)

// Update list when filters change
useEffect(() => {
  listRef.current?.handleRefresh()
}, [filters])

const fetchData = useCallback(
  (page: number, pageSize: number) =>
    fetchItems(page, pageSize, filters),
  [filters],
)

return (
  <PaginatedList<Item>
    ref={listRef}
    fetchData={fetchData}
    renderItem={renderItem}
  />
)

4. External Refresh Control (Header Button)

const listRef = useRef<PaginatedListRef>(null)

return (
  <View style={{ flex: 1 }}>
    <Header
      onRefresh={() => listRef.current?.handleRefresh()}
      isRefreshing={listRef.current?.refreshing}
    />
    <PaginatedList
      ref={listRef}
      fetchData={fetchData}
      renderItem={renderItem}
      useExternalRefreshControl={true}
    />
  </View>
)

5. Horizontal Scrolling (Carousel)

<PaginatedList<Item>
  fetchData={fetchItems}
  renderItem={({ item }) => <ItemCard item={item} />}
  horizontal={true}
  pageSize={5}
  showsHorizontalScrollIndicator={false}
/>

6. Custom Footer Component

<PaginatedList<Item>
  fetchData={fetchData}
  renderItem={renderItem}
  ListFooterComponent={
    <View style={{ padding: 20, alignItems: 'center' }}>
      <Text style={{ color: '#999' }}>End of list</Text>
    </View>
  }
/>

⚡ Performance Optimization Tips

1. Use keyExtractor for stable keys

// ✅ GOOD - Stable unique identifier
<PaginatedList
  keyExtractor={(item) => item.id}
  {...props}
/>

// ❌ AVOID - Index-based keys cause re-renders

2. Memoize render items for complex components

const DishItem = React.memo(({ item }: { item: Dish }) => (
  <View style={{ padding: 12 }}>
    <Image source={{ uri: item.imageUrl }} />
    <Text>{item.name}</Text>
  </View>
))

<PaginatedList
  renderItem={({ item }) => <DishItem item={item} />}
  {...props}
/>

3. Optimize fetchData function

// ✅ GOOD - Memoized with useCallback
const fetchData = useCallback(
  (page: number, pageSize: number) =>
    fetchFromAPI(page, pageSize, filters),
  [filters], // Only recreate when filters change
)

<PaginatedList fetchData={fetchData} {...props} />

4. Appropriate page sizes

// Images/complex items - smaller page size
<PaginatedList pageSize={10} {...props} />

// Simple text items - larger page size
<PaginatedList pageSize={50} {...props} />

🚨 Error Handling

The component automatically handles errors with user-friendly messages:

const fetchData = async (page: number, pageSize: number) => {
  try {
    const response = await fetch(
      `https://api.example.com/items?page=${page}`
    )
    
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`)
    }
    
    return response.json()
  } catch (error) {
    // Error message will be displayed in the list
    throw new Error(
      error instanceof Error 
        ? error.message 
        : 'Failed to fetch items'
    )
  }
}

<PaginatedList fetchData={fetchData} {...props} />

Error UI includes:

  • Error message text
  • Retry button (automatically calls handleRefresh)
  • Preserves previously loaded data

🔧 Troubleshooting

List Not Scrolling

// ❌ Issue: Parent doesn't have flex or height
<View>
  <PaginatedList {...props} />
</View>

// ✅ Fix: Add flex to parent
<View style={{ flex: 1 }}>
  <PaginatedList {...props} />
</View>

Items Not Rendering

// Verify keyExtractor returns unique strings
const fetchData = async (page, pageSize) => {
  const data = await fetch(`/api/items?page=${page}`)
  console.log('Fetched data:', data) // Debug
  return data.json()
}

<PaginatedList
  fetchData={fetchData}
  keyExtractor={(item) => {
    console.log('Key for item:', item._id) // Verify unique keys
    return item._id
  }}
  {...props}
/>

Refresh Not Working

// Verify ref is properly attached
const listRef = useRef<PaginatedListRef>(null)

const handleRefresh = () => {
  console.log('Refreshing...', listRef.current)
  listRef.current?.handleRefresh()
}

<PaginatedList ref={listRef} {...props} />

Infinite Loading

// Ensure fetchData returns correct amount of items
const fetchData = async (page: number, pageSize: number) => {
  const response = await fetch(
    `/api/items?page=${page}&limit=${pageSize}`
  )
  const data = await response.json()
  
  // Must return array of items (not wrapped object)
  console.log('Items returned:', data.length, 'Expected:', pageSize)
  return Array.isArray(data) ? data : data.items
}

📱 TypeScript Support

Fully typed with TypeScript generics:

import {
  PaginatedList,
  PaginatedListProps,
  PaginatedListRef,
} from 'paginated-flatlist'

// Define your data type
interface Product {
  id: string
  name: string
  price: number
}

// Typed component
<PaginatedList<Product>
  fetchData={async (page, pageSize) => {
    // TypeScript knows this should return Product[]
    return fetch(`/api/products?page=${page}`)
      .then(r => r.json())
  }}
  renderItem={({ item }) => {
    // TypeScript knows item is Product
    return <Text>{item.name}</Text>
  }}
/>

// Typed ref
const listRef = useRef<PaginatedListRef>(null)

🎨 Styling & Customization

Fully customizable with React Native styles:

<PaginatedList
  // Custom separators
  ItemSeparatorComponent={() => (
    <View style={{ height: 1, backgroundColor: '#e0e0e0' }} />
  )}
  
  // Custom empty state
  ListEmptyComponent={
    <View style={{ padding: 40, alignItems: 'center' }}>
      <Text>No results found</Text>
    </View>
  }
  
  // Custom footer
  ListFooterComponent={
    <View style={{ padding: 20 }}>
      <Text>End of list</Text>
    </View>
  }
  
  // FlashList props pass through
  contentContainerStyle={{ paddingBottom: 20 }}
  scrollEventThrottle={16}
  
  {...props}
/>

📊 State Management Integration

Works seamlessly with popular state managers:

// Redux
const items = useSelector(selectItems)
const filters = useSelector(selectFilters)

const fetchData = (page, pageSize) =>
  fetchItemsAPI(page, pageSize, filters)

// Zustand
const filters = useFiltersStore((state) => state.filters)

// Context
const { filters } = useContext(FilterContext)

🔗 Lifecycle & Hooks

useEffect(() => {
  // 1. Component mounts → Initial fetch (page 1)
  
  // 2. User scrolls → onEndReached → fetchData with page++
  
  // 3. User pulls → RefreshControl → handleRefresh
  
  // 4. Filters change → manually call handleRefresh via ref
  
  // 5. Cleanup on unmount
}, [])

🤝 Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Commit changes: git commit -m 'Add amazing feature'
  4. Push branch: git push origin feature/amazing-feature
  5. Open a Pull Request

Development

# Install dependencies
npm install

# Build TypeScript
npm run build

# Watch for changes
npm run dev

# Type check
npm run type-check

# Lint code
npm run lint

# Format code
npm run format

📝 Changelog

See CHANGELOG.md for version history and breaking changes.

📄 License

MIT © 2024

🆘 Support & Issues

🙏 Acknowledgments

  • Built with Shopify's FlashList for superior performance
  • Inspired by React Native community best practices
  • Thanks to all contributors and users!

🔗 Related Projects