Skip to main content

StaticListContainer

Non-scrolling twin of ListContainer — same props, same layout, no scroll view of its own.

ListContainer wraps a FlatList, which brings its own scroll view. Nesting one inside a ScrollContainer (or any other scroller) means turning that off with scrollEnabled={false}, and that is where things go wrong on web: react-native-web answers scrollEnabled={false} with touch-action: none and overflow: hidden on the list element. The browser then refuses to pan the parent scroller for any touch that starts over the list, so on mobile web the screen cannot be scrolled at all — and anything taller than the list's box is clipped away on top of that.

A nested list is never virtualised anyway, so StaticListContainer skips the FlatList and renders the rows directly.

Basic Usage​

import { ScrollContainer, StaticListContainer } from '@multinaire/expo-ui';

<ScrollContainer flex={1}>
<StaticListContainer
data={items}
renderItem={({ item }) => <ItemCard data={item} />}
/>
</ScrollContainer>

Props​

Exactly the props of ListContainer, so swapping one for the other is the component name and nothing else.

PropTypeDefaultDescription
dataT[]—Array of items to render
renderItem({ item, index }) => ReactElement—Render function for each item
isLoadingbooleanfalseShow loading skeleton items in place of content
itemHeightnumberheight.mediumEstimated item height in pixels, used for loading skeletons
itemCountnumber8Number of skeleton items to show while loading
flexnumber—Flex grow value
marginSpacingProps—Outer margin
paddingSpacingProps—Inner padding
gapnumbergap.mediumGap between items in pixels

The FlatList props that describe content are supported as well: keyExtractor, numColumns, columnWrapperStyle, ItemSeparatorComponent, ListEmptyComponent, ListHeaderComponent and ListFooterComponent (with their ListHeaderComponentStyle / ListFooterComponentStyle), style (the outer view) and contentContainerStyle (the inner one) — each behaving as it does on ListContainer, including keyExtractor defaulting to an item's key, then its id, then its index.

The rest of FlatList's API describes scrolling, and there is no scroll view here to apply it to: ref, horizontal, refreshControl, onEndReached, onScroll, stickyHeaderIndices, the virtualization window props, and their like are accepted — so a drop-in swap keeps type-checking — and ignored. A ListHeaderComponent will not stick, and pull-to-refresh belongs on the surrounding ScrollContainer.

Migrating from a nested ListContainer​

The pattern this replaces is a ListContainer told not to scroll, usually with nestedScrollEnabled alongside it to keep Android's nested scrolling working:

// Before — fine on native, broken on mobile web.
<ScrollContainer flex={1}>
<ListContainer
scrollEnabled={false}
nestedScrollEnabled
data={items}
renderItem={renderItem}
/>
</ScrollContainer>

// After.
<ScrollContainer flex={1}>
<StaticListContainer data={items} renderItem={renderItem} />
</ScrollContainer>

Both of those props are among the ones StaticListContainer accepts and ignores, so leaving them in place while you migrate changes nothing — but there is no scroll view left to enable or disable, so they are only noise.

StaticListContainer or ListContainer?​

Use StaticListContainer when something else scrolls: a ScrollContainer screen, a card, a popup body, a tab panel. Use ListContainer when the list is the scroller — it virtualises, so it is the only one of the two fit for a long or unbounded list, and it is where pull-to-refresh, sticky headers, and onEndReached do their work.

// The screen scrolls — the list shouldn't.
<ScrollContainer flex={1}>
<StaticListContainer data={members} renderItem={renderMember} />
</ScrollContainer>

// The list scrolls — and virtualises 10,000 rows.
<ListContainer flex={1} data={transactions} renderItem={renderTransaction} />

Examples​

Inside a scrolling screen​

The list renders in place, so the whole screen scrolls as one — headings, list, and anything after it.

<ScrollContainer flex={1} padding={variables.padding.medium}>
<Typography type="title" weight="bold">
Team
</Typography>

<StaticListContainer
margin={{ vertical: variables.padding.medium }}
data={members}
keyExtractor={(member) => member.id}
renderItem={({ item }) => (
<MenuButton title={item.name} trailing="ArrowRight2" />
)}
/>

<Button title="Invite someone" onPress={invite} />
</ScrollContainer>

Inside a card​

<Card padding={variables.padding.medium} gap={variables.gap.medium}>
<Typography type="title" weight="medium">
Recent activity
</Typography>

<StaticListContainer
data={events}
keyExtractor={(event) => event.id}
renderItem={({ item }) => <Typography>{item.label}</Typography>}
/>
</Card>

As a grid​

numColumns chunks the items into rows separated by gap, whose cells are spaced by columnWrapperStyle (a gap.medium gap by default). As on ListContainer, the row itself adds no width to a cell — give the item flex={1} to make it share the row evenly, and expect a last row that isn't full to stretch its cells to fill it.

<StaticListContainer
numColumns={2}
data={photos}
keyExtractor={(photo) => photo.id}
renderItem={({ item }) => <Photo flex={1} source={item.uri} height={120} />}
/>

With a loading state​

Skeletons follow numColumns too, so a grid keeps its shape while it loads. Your renderItem and keyExtractor are not called while isLoading is true, so neither has to cope with a placeholder row.

<StaticListContainer
isLoading={isLoading}
itemCount={6}
itemHeight={80}
data={products}
keyExtractor={(product) => product.id}
renderItem={({ item }) => <ProductCard product={item} />}
/>

With an empty state​

ListHeaderComponent and ListFooterComponent still render around it, exactly as they do on a FlatList.

<StaticListContainer
data={items}
renderItem={({ item }) => <ItemCard data={item} />}
ListEmptyComponent={
<Placeholder
graphic={<Icon icon="Box" size={48} color="neutral" />}
title="Nothing here yet"
description="Items you add will show up in this list."
/>
}
/>

With separators instead of a gap​

ItemSeparatorComponent renders between rows — between rows, not cells, when numColumns is set.

<StaticListContainer
gap={0}
data={settings}
keyExtractor={(setting) => setting.key}
renderItem={({ item }) => <MenuButton title={item.title} />}
ItemSeparatorComponent={() => <Divider />}
/>