React Native generic list

Dec 11, 2021

React Native’s FlatList is a component that supports many cool features while being simple and easy to use, it’s a good example of a well-designed piece of software and that’s why I decided to take it a step further.

For that, I’ve developed a generic list built on top of it that handles pagination and refreshing internally, reducing a lot of repetitive code from my previous implementation.

I looked for something like this online but couldn’t find anything similar, even though it might be useful for many people, so I decided to share it.

Here’s the snippet with the entire code, let’s take a brief look and review it in detail later:

import React, { useCallback, useEffect, useState } from "react";
import { FlatList, FlatListProps } from "react-native";

interface ID {
	id: string
}

type FLProps<T> = Omit<
	FlatListProps<T>, 
	"data" | "keyExtractor" | "onEndReached" | "onRefresh" | "refreshing"
>

interface Props<T> extends FLProps<T> {
	fetchItems: (cursor?: string) => Promise<[string, T[]]>
}

export const List = <T extends ID>(props: Props<T>) => {
	const [items, setItems] = useState<readonly T[]>();
	const [cursor, setCursor] = useState<string>();
	const [refreshing, setRefreshing] = useState<boolean>(false);

	const getItems = useCallback(async () => {
		try {
			const [nextCursor, elems] = await props.fetchItems(cursor);
			setCursor(nextCursor);
			items ? setItems(items.concat(elems)) : setItems(elems);
		} catch (err) {
			console.log(err);
		} finally {
			setRefreshing(false);
		}
	}, []);

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

	return (
		<FlatList
			data={items}
			keyExtractor={item => item.id}
			onEndReached={({ distanceFromEnd }) => {
				distanceFromEnd < 0 ? undefined : getItems();
			}}
			onEndReachedThreshold={
				props.onEndReachedThreshold ? props.onEndReachedThreshold : 0.1
			}
			refreshing={refreshing}
			onRefresh={() => {
				setRefreshing(true);
				setCursor(undefined);
				setItems(undefined);
				getItems();
			}}
			{...props}
		/>
	);
};

List in action

import { List } from "./List"

type User = {
  id: string,
  username: string
}

export const UserList = () => {
	const getUsers = async (cursor?: string): Promise<[string, User[]]> => {
		const response = await fetch(`localhost:4000/users?cursor=${cursor}`);
		// In this example, the response body contains a JSON object with 
		// the next cursor and an array of users
		const json = await response.json() as {next_cursor: string, users: User[]};
		return [json.next_cursor, json.users];
	}

	return (
		<List<User>
			fetchItems={(cursor) => getUsers(cursor)}
			renderItem={({ item }) => <View><Text>{item.username}</Text></View>}
			numColumns={2}
			// More properties from FlatList may be included here
		/>
	);
}

As you can see, it’s extremely easy to use and requires few lines of code to have it working.

Note two things, <User> makes explicit the type of items the list will contain and next_cursor must always return a non-null value, if not, the request will return duplicated items.

Component breakdown

Properties

interface ID {
	id: string
}

type FLProps<T> = Omit<
	FlatListProps<T>, 
	"data" | "keyExtractor" | "onEndReached" | "onRefresh" | "refreshing"
>

interface Props<T> extends FLProps<T> {
	fetchItems: (cursor?: string) => Promise<[string, T[]]>
}

export const List = <T extends ID>(props: Props<T>) => {}

The ID interface is used for the generic to accept only items that has and id field in it, which is used to extract a unique key from each of them.

FLProps contains all the properties of a FlatList except the ones that are specified with literal strings, preventing the caller from overwriting the properties that are automatically handled by the component.

In my case I was always using the types’ id field to uniquely identify items inside the list but if it’s not your case, keyExtractor can be delegated.

Lastly, the component’s properties are all of FLProps plus fetchItems, the callback from which the list will be populated.

State

const [items, setItems] = useState<readonly T[]>();
const [cursor, setCursor] = useState<string>();
const [refreshing, setRefreshing] = useState<boolean>(false);
  • items holds the elements list, it’s set to read-only as the items themselves won’t be modified. Always try to type as much as possible.
  • cursor contains the id of the element used to tell the server the starting point for a new list of elements.
  • refreshing stores a boolean that tells whether the list is waiting for more values or not.

Get items callback

const getItems = useCallback(async () => {
	try {
		const [nextCursor, elems] = await props.fetchItems(cursor);
		setCursor(nextCursor);
		items ? setItems(items.concat(elems)) : setItems(elems);
	} catch (err) {
		console.log(err);
	} finally {
		setRefreshing(false);
	}
}, []);

Here’s where most of the list’s work is done, we fetch items from a source - tipically an HTTP request to a server - using a cursor (initially undefined), so we will never get a duplicated item.

useCallback returns a memoized version of the callback that changes only when one of its dependencies has changed.

In this case it has none to execute the callback only when we explicity specify it and to prevent unnecessary renders from useEffect.

The received cursor is stored for use in following calls and, if there are already stored items, the ones from the next request are appended to them.

Any potential error is catched and logged into the console.

Finally, refreshing is set to false to tell the list that we are done getting new values.

Use effect hook

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

useEffect runs once and gets the items that the list will contain when the component is mounted, the cursor will always be undefined.

Pagination

<_
	onEndReached={({ distanceFromEnd }) => {
		distanceFromEnd < 0 ? undefined : getItems();
	}}
	onEndReachedThreshold={
		props.onEndReachedThreshold ? props.onEndReachedThreshold : 0.1
	}
/>

onEndReached is the callback that will be called whenever the user reaches the end of the list, in this case it will request more items to the server, using the cursor to specify the last item of the latest response.

There are some scenarios when the list is first rendered but it has so few items that it triggers the onEndReached callback, forcing another render.

In order to avoid this, distanceFromEnd (always negative on the first render) is set to undefined in those cases.

Refreshing

<_ 
	refreshing={refreshing}
	onRefresh={() => {
		setRefreshing(true);
		setCursor(undefined);
		setItems(undefined);
		getItems();
	}}
/>

Each time the list is pulled the onRefresh callback is triggered.

In it, the list is set to its refreshing state, the component’s state is reset and new items are requested. It’s like simulating the component’s first render.

Potential enhancements

This is a simplified version of the List component I use so there are things that may be missing for your implementation.

One kind of obvious is the possibility that fetchItems returns undefined data.

Some others may be:

  • Passing more parameters to the fetch items callback.
  • Set a limit to the amount of items that can be stored.
  • Use a reference to a boolean value to avoid requesting items on an unmounted list.

Extending its utility it’s up to your imagination and I hope this post has inspired you to create useful generic components.