- engineering blog
- Product
8 min read
React Data List: Building virtualized UIs declaratively

Braden Marshall Senior Product Engineer
React Data List is an open-source library we built to help you construct arrays by expressing items declaratively using React components. It is built primarily for virtualizers in React Native, but it is platform-agnostic and works everywhere React does, including on the web.
You might be thinking: “Why would I want to do that?”
The reason mostly comes down to ergonomics. It allows you to use your project’s existing React UI tools (such as hooks, conditional rendering, and Suspense) to compose arrays of data. This makes it easier to virtualize your lists and consequently encourages you to virtualize more often.
What’s the problem?
When building apps, you’ll inevitably run into performance issues when rendering large scrollable lists. The first port of call is usually to employ a virtualizer, and there are dozens of great ones out there. These work by avoiding the rendering of your entire list of items and instead rendering only what’s currently in the viewport (often with some overshoot to help prevent blanks when scrolling). For the sake of this article, we’ll focus on React Native, though the same idea applies to other platforms such as the web.
In React Native, there is a built-in virtualizer called FlatList. This is a component which takes an array of data
(the items you want to render), a renderItem
function (how to render an item), and a keyExtractor
(how to uniquely identify an item in the list, to track reordering, etc.).
The API for FlatList is quite universal, with the more sophisticated recycling-based virtualizers (such as FlashList and LegendList) offering an almost identical API.
Typically, your data arrays start off looking something like this:
const data = [
{type: "header", title: "The Fellowship"},
{type: "character", name: "Frodo"},
{type: "character", name: "Aragorn"},
{type: "character", name: "Legolas"},
{type: "character", name: "Gimli"},
]
You can imagine type
will be used to drive a switch case in the renderItem
, keyExtractor
, etc. As we introduce more item types to our list, this will, of course, grow. This works well for simple and deterministic cases like the one above, where we can treat our data array as an atomic piece. Real-world applications aren’t always like that.
We often need to pull data from multiple sources, filter the items based on some state or user permissions, hide section headers if there is no corresponding content, and handle pending and error states (including refetching, without poisoning the entire list state and replacing everything with a spinner). Things get messy fast. If you battle through and address all of those concerns, you'll likely end up with one of two things.
A cumbersome mess, often a single hook that’s a thousand lines long. It’s terrifying to make changes and a struggle to extract list sections and then compose them together somewhere else.
An abstraction.
An abstraction surely sounds better. But why do we need to look outside of React for this? Ultimately, these are just UI problems, and UI is what we’re rendering. Plus, if we could construct this with React components, we could then start to use React’s other goodies like conditional rendering, hooks, React.Suspense and Error Boundaries. It would also plug more naturally into whatever data-fetching library or context our project already uses.
As further motivation, Shopify recently discussed a strategy called LazyScrollView, where they divide most of their screens into sections to be consumed by their virtualizer FlashList. This inspired us to look at our app from a different lens – we realized most screens are really just lists: a set of vertically stacked boxes. We only avoid implementing them this way because managing data arrays is such a hassle and doesn’t align with how we usually think about UI (that being React components).
React Data List: 1000-foot view
Suppose we wanted to build a Wikipedia-style app for Middle Earth fans.
Below is an example of how this might look:
A horizontal list of places.
A list of the Fellowship characters.
The order of which can be sorted or jumbled.
A list of Thorin’s Company.
This section loads asynchronously compared to everything else, because dwarves are slow.
In this example, the dataset is obviously small, but reality isn’t Middle Earth - we don’t always know how much data we’ll need to display and can’t always make guarantees around how that data is obtained. We have only three sections here, but what if the content is dynamic and unbounded? What if we want to reuse these sections on other pages?
Solving these kinds of problems with traditional data arrays means leaning almost entirely on vanilla JavaScript. This would likely be an array composed of lots of nested spreading, .filter
, .sort
, and plenty of ternaries to handle the various states. It gets quite messy and there is a ton of freedom.
React Data List tries to be more opinionated about this and allows you to express your logic using React. The component code for the above example will look something like this:
<ReactDataList
renderer={
<FlashListRenderer
contentContainerStyle={contentContainerStyle}
ListHeaderComponent={
<Header
toggleThorinAndCompany={toggleThorinAndCompany}
jumbleFellowship={jumbleFellowship}
/>
}
/>
}
>
<ListHeaderDataListRow title="Places in Middle Earth" />
{/* The component below is actually a nested ReactDataList instance */}
<MiddleEarthPlacesDataListRow placeItems={MIDDLE_EARTH_PLACES} />
{/* We can map over list items as if we weren't even virtualizing */}
<ListHeaderDataListRow title="The Fellowship" />
{fellowship.map((character) => (
<CharacterListItemDataListRow
key={character.name}
name={character.name}
url={character.url}
/>
))}
{/* We can conditionally render to add or remove data from the list. */}
{isShowingThorinAndCompany && (
<>
<ListHeaderDataListRow title="Thorin and Company" />
{/* We can suspend with rows and handle that here. */}
<React.Suspense
fallback={
<>
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
</>
}
>
{/* This component attaches a bulk of rows. */}
{/* It can easily be reused across lists. */}
<MiddleEarthHobbitCompanyDataListRows />
</React.Suspense>
</>
)}
</ReactDataList>
This declarative approach to adding items to our list feels much more like writing React UI code.
Below, you’ll see how the CharacterListItemDataListRow
component looks. It uses ReactDataList.Row
to register an item object, its corresponding render function and a recycler type. While you can render out as many of these as you like, there is also a ReactDataList.Rows
for attaching a bulk of items. What’s cool is that this component is totally unaware of the wider list, so it can be dropped in and reused in any other data list. The value here is that it allows you to solve the data array problem using the same patterns you already use for your UI (Suspense, hooks, context, etc.).
Keep in mind that this is still just an ordinary React component, so you can fill it with whatever data-fetching library you like. These components are only used to attach data to the list, and they resolve to null
. Therefore, there are no presentational components. We love components like this at Attio because they let us use React’s tools and control flow to solve a broader set of problems.
const renderListItem: ReactDataList.RenderListItem<CharacterListItemItem> = (itemInfo) => {
{/* We have helpers to adjust rendering based on the list as a whole. */}
const {itemIndex, totalItems} = getRelativePositionPerRecyclerType(itemInfo)
const item = itemInfo.descriptor.item
const isFirst = itemIndex === 0
const isLast = itemIndex === totalItems - 1
return (
<CharacterListItem
name={item.name}
url={item.url}
style={styles.container}
isFirst={isFirst}
isLast={isLast}
/>
)
}
export function CharacterListItemDataListRow({name, url}: Omit<CharacterListItemItem, "id">) {
const id = React.useId()
return (
<ReactDataList.Row
id={id}
item={React.useMemo(() => ({id, name, url}), [name, url])}
render={renderListItem}
recyclerType="character-list-item"
/>
)
}
You might wonder what the FlashListRenderer
component is. React Data List isn’t built for any specific virtualizer. It’s designed to be portable and to solve a more generic problem - use React to build an array of items. As you can see below, it’s nothing too menacing - just a lightweight wrapper around FlashList
. It’s easy to adapt for use with other virtualizers, such as FlatList or LegendList.
interface FlashListRendererProps<TRenderItem> extends Omit<
FlashListProps<ReactDataList.RenderListItemInfo<TRenderItem>>,
"data" | "renderItem" | "keyExtractor" | "getItemType" | "ListEmptyComponent"
> {}
export function FlashListRenderer<TRenderItem>(props: FlashListRendererProps<TRenderItem>) {
const {data, rootRenderItem, renderEmpty, getItemId} = useDataListRendererContext()
const getItemType = React.useCallback(
(item: ReactDataList.RenderListItemInfo<TRenderItem>) => item.descriptor.recyclerType,
[]
)
return (
<FlashList
data={data}
renderItem={rootRenderItem}
keyExtractor={(item) => getItemId(item)}
getItemType={getItemType}
ListEmptyComponent={renderEmpty}
{...props}
/>
)
}
ReactDataList - fetchable template
For Attio’s mobile app, most of our screens follow a similar structure: a list with top-level loading and error states. More often than not, our top-level loading state displays skeletons in place of the items. Our internal data fetching library utilizes React.Suspense and Error Boundaries to manage the control flow.
We built a small template around ReactDataList
called ReactDataList.Fetchable
. This offers a few props that are useful for typical async work, such as displaying skeleton rows (via renderPendingRows
), a full-screen spinner (via renderPending
), or a full-screen error message (via renderError
).
If Suspense and Error Boundaries are tools you already use, then this should be helpful. It’s also a good example of how you can wrap ReactDataList
to provide an API that aligns more closely with your project’s own data model.
Usage looks like this:
<ReactDataList.Fetchable
renderer={
<FlashListRenderer
contentContainerStyle={contentContainerStyle}
ListHeaderComponent={<Header reload={reload} />}
/>
}
/**
* This catches the suspense signal from below and gives us a
* chance to attach a different set of rows.
*/
renderPendingRows={
<>
<ListHeaderDataListRow title="Thorin and Company" />
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
<LoadingListItemDataListRow />
</>
}
>
<ListHeaderDataListRow title="Thorin and Company" />
{/* This component throws a suspense signal while loading */}
<MiddleEarthHobbitCompanyDataListRows key={reloadKey} />
</ReactDataList.Fetchable>
In a post-ReactDataList world
Right now, we’re working on a redesign of our mobile app at Attio. We wanted finer-grained control around data-loading without sacrificing performance. React Data List has allowed that. It’s made it easy to build virtualization into pretty much every screen, which has made a night and day difference to our app’s performance. It has also done so without forcing us to make hard tradeoffs around the composability of our UI code.
Everything continues to feel like React and to come with the same reusability characteristics. ReactDataList
is light on introducing new concepts: keeping the focus on React and our internal data-fetching library. Previously, our functions for composing data lists were tailored to individual screens, which made it harder than it needed to be for engineers to switch between them.
If you want to dive into this example further, you can find it here inside the GitHub repo for @attio/react-data-list
.
Interested in shipping powerful systems and tackling challenges like these? Come build with us.