How to use Infinite Scroll with FlatList and React Hook?

Viewed 5248

I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.

const [page, setPage] = useState(1);

This is my useEffect Hook:

useEffect(() => {
  const loadProducts = async () => {
    setIsLoading(true);
    let response = await fetch(`${api}&page=${page}&perPage=5`);
    let results = await response.json();
    setProducts([...products, ...results.data]);
    setIsLoading(false);
  };
  loadProducts();
}, [page]);

Offset is ${page}, limit is &perPage=5 (hardcoded to 5)

Flatlist:

<FlatList
  data={products}
  keyExtractor={(item) => item.id}
  renderItem={renderGridItem}
  onEndReached={loadMore}
  onEndThreshold={0.3}
/>;

LoadMore:

const loadMore = () => {
  setPage(page + 1);
};

In theory, this should work, shouldn't it?

2 Answers

Description

I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)

The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.

In this example we use reqres.in to simulate a fetch to some data.

The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.

Expo snack

A snack of the example can be found here: https://snack.expo.io/BydyF9yRH

Make sure to change platform to iOS or Android in the snack (Web will not work)

Code

import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')

import {
    StyleSheet,
    Text,
    View,
    SafeAreaView,
    SectionList,
    Button,
    RefreshControl
} from 'react-native';

function Item(item) {
    return (
        <View style={styles.item}>
            <Text style={styles.title}>{item.title.first_name}</Text>
        </View>
    );
}

export default function testSectionList({ navigation }) {

    const [data, setData] = React.useState()
    const [loading, setLoading] = React.useState(true)
    const [refreshing, setRefreshing] = React.useState(false);
    const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);

    const dataIndex = React.useRef(0);
    const totalHits = React.useRef(42); // In real example: Update this with first result from api

    const fetchData = async (reset: boolean) => {

        if (reset === true) dataIndex.current = 0;

        // Make sure to return if no more data from API
        if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []

        // For example usage, select a random page
        const fakepage = Math.round(Math.random()) * 2
        const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
        const result = await resultObject.json()

        dataIndex.current++;

        return {
            title: `${dataIndex.current-1}`,
            data: await result.data
        }
    }

    const count = () => {
        alert(data.length)
    }

    const checkPageNumbers = () => {
        const numbers = data.map(item => parseInt(item.title))
        const incremental = [...Array(data.length).keys()]

        alert(_.isEqual(numbers, incremental))
    }

    const getInitialData = async () => {

        const list = await fetchData(false)
        if(!list) return

        setData([list])

        setLoading(false)
    }

    React.useEffect(() => {
        getInitialData()
    }, [])

    const onEndReached = async () => {
        const newItems = await fetchData(false)
        if(!newItems.data.length) return
        setData([...data, newItems])
    }

    const onRefresh = React.useCallback(async () => {
        setShowRefreshingIndicator(true);

        const newItems = await fetchData(true)
        setData([newItems])

        setShowRefreshingIndicator(false)

    }, [refreshing]);

    if (loading) return <Text>Loading...</Text>

    return (
        <SafeAreaView style={styles.container}>
            <Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
            <Button title={"Check length"} onPress={() => count()} />
            <SectionList
                sections={data}
                refreshing={refreshing}
                refreshControl={
                    <RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
                }
                onEndReached={() => {
                    if(refreshing) return;
                    setRefreshing(true)
                    onEndReached().then(() => {
                        setRefreshing(false)
                    })
                }}
                onEndReachedThreshold={1}
                keyExtractor={(item, index) => item + index}
                renderItem={({ item }) => <Item title={item} />}
                renderSectionHeader={({ section: { title } }) => (
                    <Text style={styles.header}>{title}</Text>
                )}
                ListFooterComponent={<ActivityIndicator size={"large"} />}
                stickySectionHeadersEnabled={false}
            />
        </SafeAreaView>
    );

}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 40,
        marginHorizontal: 16,
    },
    item: {
        backgroundColor: '#f9c2ff',
        padding: 2,
        marginVertical: 2,
    },
    header: {
        fontSize: 16,
    },
    title: {
        fontSize: 12,
    },
});

Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.

const loadProducts = async () => {
  setIsLoading(true);
  let response = await fetch(`${api}&page=${page}&perPage=5`);
  let results = await response.json();
  if (result.data) {
    setProducts([...products, ...results.data]);
  }
  setIsLoading(false);
};

useEffect(() => {
  loadProducts();
}, [])

const onLoadMore = useCallback(() => {
  loadProducts();
}

for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback

Related