Usage
Legend List is a virtualized ScrollView component for React Native with optional recycling, that can massively increase performance of rendering long lists. Rather than rendering every item in the list, it only renders the items that are in view, which significantly reduces the amount of items that need to render.
Legend List is a drop-in replacement for FlatList or FlashList. So since you’re likely coming from one of those, we’ll start with a guide on how to switch.
Switch from FlashList
If you’re coming from FlashList, in most cases you can just rename the component and it will work as expected. But note that Legend List does not recycle items by default, so to match FlashList’s behavior you can enable recycleItems
.
return ( <FlashList <LegendList data={items} renderItem={({ item }) => <Text>{item.title}</Text>} estimatedItemSize={320} recycleItems />)
Switch from FlatList
If you’re coming from FlatList, you’ll just want to add at least one prop, the estimatedItemSize
. Legend List automatically lays out your items in a very high performance way, and a hint for what size the items are likely to be will help it be as fast as possible.
return ( <FlatList <LegendList data={items} renderItem={({ item }) => <Text>{item.title}</Text>} estimatedItemSize={320} />)
If you first render a LegendList without an estimatedItemSize
it will suggest an optimal item size based on the rendered items, so that can help you find the right number.
Guide
Legend List is very optimized by default, so it may already be working well without any configuration. But these are some common ways to improve your list behavior.
Estimate item sizes
estimatedItemSize?: number;getEstimatedItemSize?: (index: number, item: T) => number;onItemSizeChanged?: (info: { size: number; previous: number; index: number; itemKey: string; itemData: ItemT; }) => void;
It’s important to provide an estimatedItemSize
(if items are the same size or all dynamic sizes) or getEstimatedItemSize
(if items are different known sizes). Legend List uses this as the default item size, then as items are rendered it updates their positions with the actual size. So getting this estimate as close as possible to the real size will reduce layout shifting and blank spaces as items render. If not provided it will use 100px
as the default.
The onItemSizeChanged
event can also help with your estimations - it will be called whenever an item’s size changes. So you can use it to log what the actual rendered size is to adjust your estimates.
Use keyExtractor
keyExtractor?: (item: T, index: number) => string;
The keyExtractor
prop lets Legend List save item layouts by key, so that if the data
array changes it can reuse previous layout information and only update the changed items. Without keyExtractor
, item sizes will reset to their default whenever data
changes. So it is very recommended to have a keyExtractor
if data
ever changes. If your items are a fixed size, providing a keyExtractor
that returns the index will tell it to reuse size information.
Set a drawDistance
drawDistance?: number
The drawDistance
(defaults to 250
) is the buffer size in pixels above and below the viewport that will be rendered in advance. So for example if your screen is 2000px
tall and your draw distance is 1000
, then it will render double your screen size, from -1000px
above the viewport to 1000px
below the viewport.
This can help reduce the amount of blank space while scrolling quickly. But if your items are computationally expensive, it may reduce performance because more items are rendering at once. So you should experiment with it to find the most optimal behavior for your app.
waitForInitialLayout
waitForInitialLayout?: boolean
If the size of your list items differs significantly from the estimate, you may see a layout jump after the first render. If so, the waitForInitialLayout
prop solves that by delaying displaying list items by one frame so they start at the correct position.
Recycling items
recycleItems?: boolean
Legend List has an optional recycleItems
prop which enables view recycling. This will reuse the component rendered by your renderItem
function. This can be a big performance optimization because it does not need to destroy/create views while scrolling. But it also reuses any local state, which can cause some weird behavior that may not be desirable depending on your app. But see the next section for recycling hooks to make that easier.
So there are some tradeoffs with recycling:
- 👍 If you have items with no state then recycling should be great
- 👎 If you have simple items with complex state then it may be more trouble than it’s worth
- 👍 If you have heavy items with complex state then working around the state recycling may be worth it for the performance gains
Recycling hooks
useRecyclingEffect: (effect: (info: LegendListRecyclingState) => void | (() => void)) => void;useRecyclingState: <T>(updateState: ((info: LegendListRecyclingState) => T) | T) => [T, Dispatch<T>];
renderItem
receives two hooks to help you manage the recycling.
useRecyclingState
automatically resets the state when it recycles into a new itemuseRecyclingEffect
can be used to reset any side effects when an item gets recycled.
export function ItemComponent({ item, useRecyclingEffect, useRecyclingState }) {
// Like useState but it resets when the item is recycled const [isExpanded, setIsExpanded] = useRecyclingState(() => false);
// A callback when the item is recycled into a new item useRecyclingEffect?.(({ item, prevItem, index, prevIndex }) => { // Reset any side effects from the previous item refSwipeable?.current?.close(); refVideo?.current?.reset(); });
// ...}
Maintain Visible Content Position
maintainVisibleContentPosition?: boolean;
The maintainVisibleContentPosition
prop automatically adjusts item positions when items are added/removed/resized above the viewport so that there is no shift in the visible content. This is very helpful for some scenarios, but if you have a static list of fixed sized items you probably don’t need it.
- If items get added/removed/resized above the viewport, items will not move on screen
- When using
initialScrollOffset
orinitialScrollIndex
, items will not jump around when scrolling up if they’re different sizes than the estimate - When scrolling to an index far down the list and then back up, items will not jump around as they layout
LegendList utilizes ScrollView’s maintainVisibleContentPosition prop internally, so your target react-native version should support that prop. To use maintainVisibleContentPosition on Android you will need at least React Native version 0.72.
Chat interfaces without inverse
alignItemsAtEnd?: boolean;maintainScrollAtEnd?: boolean;maintainScrollAtEndThreshold?: number;
In other list libraries if you wanted items to start scrolling from the bottom, you’d need to use an inverted
prop, which would apply a negative scale transform. But that causes a lot of weird issues, so Legend List explicitly does not do that.
Instead, to align items at the end you can just use the alignItemsAtEnd
prop, which will apply padding above items to fill the screen and stick them to the bottom.
The maintainScrollAtEnd
prop will check if you are already scrolled to the bottom when data
changes, and if so it keeps you scrolled to the bottom.
The maintainScrollAtEndThreshold
prop (which defaults to 0.1) defines what percent of the screen counts as the bottom.
So using Legend List for a chat interface would look like this:
<LegendList data={items} renderItem={({ item }) => <Text>{item.title}</Text>} estimatedItemSize={320} alignItemsAtEnd maintainScrollAtEnd maintainScrollAtEndThreshold={0.1} />
Two-way infinite scrolling
onStartReached?: ((info: { distanceFromStart: number }) => void) | null | undefined;onEndReached?: ((info: { distanceFromEnd: number }) => void) | null | undefined;
These callbacks fire when you scroll to the top or bottom of a list. This can be used to load more data in either direction. In a typical list you’ll likely just use onEndReached
to load more data when the users scrolls to the bottom.
If you have a chat-like interface you may want to load more messages as you scroll up, and you can use onStartReached
for that. If you are doing that, you will very likely want to use maintainVisibleContentPosition so that the items loading above don’t shift the viewport down.
Props
alignItemsAtEnd
alignItemsAtEnd?: boolean;
Aligns to the end of the screen, so if there’s only a few items there will be enough padding at the top to make them appear to be at the bottom. See Chat interfaces without inverse for more.
data
data: ItemT[];
An array of the items to render. This can also be an array of keys if you want to get the item by key in renderItem.
drawDistance
drawDistance?: number;
The drawDistance
(defaults to 250
) is the buffer size in pixels above and below the viewport that will be rendered in advance. See drawDistance for more.
estimatedItemSize
estimatedItemSize?: number;
An estimated size for all items which is used to estimate the list layout before items actually render. If you don’t provide this, it will log a suggested value for optimal performance.
getEstimatedItemSize
getEstimatedItemSize?: (index: number, item: ItemT) => number;
An estimated size for each item which is used to estimate the list layout before items actually render. If you don’t provide this, it will log a suggested value for optimal performance.
initialScrollIndex
initialScrollIndex?: number;
Start scrolled with this item at the top. By default, to have accurate scrolling position you will need to provide accurate element positions to the getEstimatedItemSize function(similar FlatList). When accurate positions are not known (e.g., for dynamically sized list items), please enable maintainVisibleContentPosition prop. This will allow LegendList to automatically adjust its top boundary when elements below initialScrollIndex will be measured.
initialScrollOffset
initialScrollOffset?: number;
Start scrolled to this offset.
ItemSeparatorComponent
ItemSeparatorComponent?: React.ComponentType<any>;
Rendered in between each item, but not at the top or bottom. By default, highlighted and leadingItem Called when the viewability of rows changes, as defined by the viewabilityConfig prop.t), or a React element.
See React Native Docs.
keyExtractor
keyExtractor?: (item: ItemT, index: number) => string;
Highly recommended. The keyExtractor
prop lets Legend List save item layouts by key, so that if the data
array changes it can reuse previous layout information and only update the changed items. See Use key extractor
ListEmptyComponent
ListEmptyComponent?: React.ComponentType<any> | React.ReactElement | null | undefined;
Called when the viewability of rows changes, as defined by the viewabilityConfig prop.eComponent />).
See React Native Docs.
ListEmptyComponentStyle
ListEmptyComponentStyle?: StyleProp<ViewStyle> | undefined;Called when the viewability of rows changes, as defined by the viewabilityConfig prop.al View for ListEmptyComponent.
See [React Native Docs](https://reactnative.dev/docs/flatlist#listemptycomponent).
### ListFooterComponent
```tsListFooterComponent?: React.ComponentType<any> | React.ReactElement | null | undefined;
Called when the viewability of rows changes, as defined by the viewabilityConfig prop.eComponent />).
See React Native Docs.
ListFooterComponentStyle
ListFooterComponentStyle?: StyleProp<ViewStyle> | undefined;Called when the viewability of rows changes, as defined by the viewabilityConfig prop.al View for ListFooterComponent.
See [React Native Docs](https://reactnative.dev/docs/flatlist#listfootercomponentstyle).
### ListHeaderComponent
```tsListHeaderComponent?: React.ComponentType<any> | React.ReactElement | null | undefined;
Called when the viewability of rows changes, as defined by the viewabilityConfig prop.nt />).
See React Native Docs.
ListHeaderComponentStyle
ListHeaderComponentStyle?: StyleProp<ViewStyle> | undefined;Called when the viewability of rows changes, as defined by the viewabilityConfig prop.al View for ListHeaderComponent.
See [React Native Docs](https://reactnative.dev/docs/flatlist#listheadercomponentstyle).
### maintainScrollAtEnd
```tsmaintainScrollAtEnd?: boolean;
This will check if you are already scrolled to the bottom when data
changes, and if so it keeps you scrolled to the bottom.
See Chat interfaces without inverse for more.
maintainScrollAtEndThreshold
maintainScrollAtEndThreshold?: number;
This defines what percent of the screen counts as the bottom. Defaults to 0.1
.
See Chat interfaces without inverse for more.
maintainVisibleContentPosition
maintainVisibleContentPosition?: boolean;
Automatically adjust item positions when items are added/removed/resized above the viewport so that there is no shift in the visible content.
See Maintain Visible Content Position for more.
numColumns
numColumns?: number;
Multiple columns will zig-zag like a flexWrap layout. Rows will take the maximum height of their columns, so items should all be the same height - masonry layouts are not supported.
onEndReached
onEndReached?: ((info: { distanceFromEnd: number }) => void) | null | undefined;
A callback that’s called only once when scroll is within onEndReachedThreshold
of the bottom of the list. It resets when scroll goes above the threshold and then will be called again when scrolling back into the threshold.
onEndReachedThreshold
onEndReachedThreshold?: number | null | undefined;
The distance from the end as a percentage that the scroll should be from the end to trigger onEndReached
. It is multiplied by screen size, so a value of 0.5 will trigger onEndReached
when scrolling to half a screen from the end.
onStartReached
onStartReached?: ((info: { distanceFromStart: number }) => void) | null | undefined;
A callback that’s called only once when scroll is within onStartReachedThreshold
of the top of the list. It resets when scroll goes below the threshold and then will be called again when scrolling back into the threshold.
onStartReachedThreshold
onStartReachedThreshold?: number | null | undefined;
The distance from the start as a percentage that the scroll should be from the end to trigger onStartReached
. It is multiplied by screen size, so a value of 0.5 will trigger onStartReached
when scrolling to half a screen from the start.
onViewableItemsChanged
onViewableItemsChanged?: OnViewableItemsChanged | undefined;
Called when the viewability of rows changes, as defined by the viewabilityConfig prop.
See React Native Docs.
recycleItems
recycleItems?: boolean;
renderItem
renderItem?: (props: LegendListRenderItemProps<ItemT>) => ReactNode;
viewabilityConfig
viewabilityConfig?: ViewabilityConfig;
Configuration for when to update the onViewableItemsChanged
callback.
See React Native Docs.
viewabilityConfigCallbackPairs
viewabilityConfigCallbackPairs?: ViewabilityConfigCallbackPairs | undefined;
List of ViewabilityConfig
/onViewableItemsChanged
pairs. A specific onViewableItemsChanged
will be called when its corresponding ViewabilityConfig
’s conditions are met.
See React Native Docs.
onItemSizeChanged
onItemSizeChanged?: (info: { size: number; previous: number; index: number; itemKey: string; itemData: ItemT; }) => void;
Called whenever an item’s rendered size changes. This can be used to adjust the estimatedItemSize to match the actual size, which can improve performance or reduce layout shifting.