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.
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | — | Array of items to render |
renderItem | ({ item, index }) => ReactElement | — | Render function for each item |
isLoading | boolean | false | Show loading skeleton items in place of content |
itemHeight | number | height.medium | Estimated item height in pixels, used for loading skeletons |
itemCount | number | 8 | Number of skeleton items to show while loading |
flex | number | — | Flex grow value |
margin | SpacingProps | — | Outer margin |
padding | SpacingProps | — | Inner padding |
gap | number | gap.medium | Gap 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 />}
/>