Boise ID
00 00
Do you think it's going to rain today?
Building an infinitely scrolling search component with Expo, React Native, and SQLite
Here's a look at some of the tools that ship with Expo and React Native to help implement high-performance, infinitely scrolling database pagination components.

While working on an Expo app, I needed to implement a component that could be used to search through a user's own saved words and the entire English dictionary simultaneously. Infinite scrolling was an obvious utility in this scenario.

React Native and Expo each provide tools for this specific use case. In this post, I'll show you how to use them together.

Database

Expo's SQLite API has methods built in for the purpose of paginating results. getEachAsync and getEachSync return Iterables that can be used to fetch rows from a query in chunks.

Here's an example of a query that fetches 10 rows:

example.ts
for await (const entry of db.getEachAsync('SELECT * FROM words')) { console.log(entry) }

This is already looking clean and minimal! But what about the question of pagination? How do we fetch more rows as needed?

useSearch hook

I created a custom React hook called useSearch that handles the pagination logic. It exposes a search function to set the search term and returns the result as well as an additional function loadNext to fetch more rows.

useSearch.ts
import { useCallback, useState, useRef } from 'react'; import { useSQLiteContext } from 'expo-sqlite'; import { parseDictionaryEntry } from '@/helpers/parseDictionaryEntry'; export function useSearch() { const dbQuery = useRef<AsyncIterableIterator<any>>(); const shouldEmptyResultsList = useRef<boolean>(false); const [result, setResult] = useState<any[]>([]); const db = useSQLiteContext(); const search = useCallback((term: string) => { dbQuery.current = db.getEachAsync( ` SELECT * FROM words WHERE words.word LIKE ? ORDER BY words.word `, [`${term}%`] ); shouldEmptyResultsList.current = true; }, [db]); const loadNext = useCallback(async (count: number) => { if (!dbQuery.current) return; const results: any[] = []; let i = 0; while (i < count) { const row = await dbQuery.current.next(); if (row.done) break; const parsedEntry = parseDictionaryEntry(row.value); results.push(parsedEntry); i++; } setResult((prev) => { if (shouldEmptyResultsList.current) { shouldEmptyResultsList.current = false; return results; } else { return [...prev, ...results]; } }); }, []); return { result, search, loadNext } }

The shouldEmptyResultsList flag is used to clear the results list only when new search results are returned. This way, the user isn't shown a flashing empty list while they're typing.

FlatList component

React Native also offers a solution specific to infinite scrolling: the FlatList component. This component is designed to render only the items that are currently visible on the screen and offers event callbacks for when the user scrolls near to the end of the list.

Here's how we integrate it with our useSearch hook:

Search.tsx
import { View, FlatList } from 'react-native'; import { SearchResult } from './SearchResult'; import { useSearch } from '@/hooks/useSearch'; import { useEffect } from 'react'; interface SearchResultsListProps { search: string; } export function SearchResultsList({ search }: SearchResultsListProps) { const dictionary = useSearch(); useEffect(() => { dictionary.search(search); dictionary.loadNext(10); }, [search]); return ( <View> <FlatList data={dictionary.result} extraData={dictionary.result} renderItem={({ item }) => <SearchResult word={item} />} keyExtractor={(item) => item.id} onEndReached={() => dictionary.loadNext(10)} onEndReachedThreshold={0.1} /> </View> ) }

In this case, the SearchResult component is a simple component that renders a word and its definition, and the search parameter is passed down from an input in the parent component.

The onEndReached callback is executed when the user scrolls to the end of the list, and then we load 10 more results.

The endReachedThreshold and number of results to load in at once should be tweaked to provide the best user experience for the details of your appβ€”the height of the items and the performance cost of loading in a batch of resultsβ€”to ensure smooth scrolling with no performance hangs.

MacCarrithers