I have a proposal for some new APIs, and a first draft on an implementation (first diff: D4412469). Salient points on the new APIs:
-
Two easy-to-use components:
FlatList
andSectionList
. I have found that separating the section API out makes things a lot cleaner for both cases. -
One shared underlying implementation,
VirtualizedList
, which has an API similar to react-virtualized and uses getter functions to be very flexible and supports any kind of data structure such as a plain array or an immutable list. -
Naming adjustments to make it horizontal agnostic, e.g. "Item" instead of "Row".
-
No more
DataSource
.FlatList
just takes an array of data. Easy peasy. keys are determined by an optionalkeyExtractor
function prop that by default looks for a key prop on your data items and falls back to using the index as the key, just like React.SectionList
takes an array of section objects, each of which has anitems
array. No more requiredrowHasChanged
function. -
Built-in PTR - just set the
onRefresh
andrefreshing
props. -
*Component
props instead ofrender*
function props (e.g.FooterComponent
vs.renderFooter
). This gives more flexibility, encourages reuse of components, and cuts down on boiler plate and possible perf issues in some circumstances. You can still provide a render function like before since they are just functional React components. -
SectionList
is more powerful. In addition to supporting sticky section headers, different sections can have differentItemComponent
s, so it's much easier to compose sections of heterogeneous components and data. -
Virtualized by default, so content outside of the render window is unmounted and the memory is reclaimed, allowing scrolling through massive amounts of data without running out of memory and also improves perf in several other ways.
-
Easier to use viewable items API.
-
Reduced implementation complexity because no longer addressing Incremental rendering - assuming React Fiber will fix that problem.
Open Questions:
- Should we add a missing key warning like React has?
- Should we pass
index
toItemComponent
s? - Should we allow arbitrary recursive nesting of sections? API needs to be nailed down in general.
- General naming and tweaks are still open for debate, especially
Viewable
. - How smart should we try to make our adaptive/predictive windowing?
- Can we rely on
onLayout
or should we try to useRCTScrollViewManager.calculateChildFrames
likeListView
? I have a perf optimization that should getonLayout
useable on both platforms - Should we do windowing in terms of component count or pixels or visible lengths?
- Should we support fixed-height optimizations.
- What kind of jump-to API's should we support? Should we support them with dynamic heights using black magic (e.g. negative insets)?
- Should we allow free scrolling through blank content, or prevent scrolling into yet-to-be-rendered areas? Would it be worth making it an option?
- I'd like to at least support simple grids, including Masonry/Pinterest style layouts - what range of use-cases should we support, and what should that API look like?
Minimal example:
const MyRow = ({item}) => <Text>{item.key}</Text>;
...
<FlatList
items={[{key: 'a'}, {key: 'b'}]}
ItemComponent={MyRow}
/>
flow interface:
type Item = any;
type Viewable = {item: Item, key: string, index: ?number, isViewable: boolean, section?: any};
// ####### FlatList ######
type RequiredProps = {
ItemComponent: ReactClass<{item: Item, index: number}>,
items: ?Array<Item>,
};
type OptionalProps = {
FooterComponent?: ReactClass<*>,
SeparatorComponent?: ReactClass<*>,
/**
* getItemLength and getItemLayout are optional optimizations that let us skip measurement of
* dynamic content if you know the height of items a priori. getItemLayout is the most efficient,
* and is easy to use if you have fixed height items, for example:
*
* getItemLayout={(items, index) => ({length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index})}
*
* Remember to include separator height in your offset calculation if you specify
* `SeparatorComponent`.
*/
getItemLayout?: (items: any, index: number) => {length: number, offset: number},
getItemLength?: (items: any, index: number) => number,
horizontal?: ?boolean,
/**
* Used to extract a unique key for a given item at the specified index. Key is used for caching
* and as the react key to track item re-ordering. The default extractor checks item.key, then
* falls back to using the index, like react does.
*/
keyExtractor?: (item: Item, index: number) => string,
/**
* The `msg` prop is used in place of ref functions like `scrollTo`. If the `msg` object changes,
* it will be processed once. A typically pattern is to store it in state, then `setState` with a
* new `msg` when you want to perform a one-time action, like scroll to a specific offset.
*/
msg?: ?(
// scrollToItem may be janky without `getItemLayout` prop. Requires linear scan through items -
// use scrollToIndex instead if possible.
{action: 'scrollToItem', animated?: ?boolean, item: Item, viewPosition?: number} |
// scrollToIndex may be janky without `getItemLayout` prop
{action: 'scrollToIndex', animated?: ?boolean, index: number, viewPosition?: number} |
{action: 'scrollToOffset', animated?: ?boolean, offset: number} |
// scrollToEnd may be janky without `getItemLayout` prop
{action: 'scrollToEnd', animated?: ?boolean}
),
/**
* Called once when the scroll position gets within onEndReachedThreshold of the rendered content.
*/
onEndReached?: ?({distanceFromEnd: number}) => void,
onEndReachedThreshold?: ?number,
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: ?Function,
/**
* Called when the viewability of rows changes, as defined by the
* `viewablePercentThreshold` prop.
*/
onViewableItemsChanged?: ?({viewableItems: Array<Viewable>, changed: Array<Viewable>}) => void,
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: ?boolean,
/**
* Optional optimization to minimize re-rendering items.
*/
shouldItemUpdate?: ?(
props: {item: Item, index: number},
nextProps: {item: Item, index: number}
) => boolean,
};
type Props = RequiredProps & OptionalProps; // plus props from the underlying implementation
// ####### SectionList #######
type Section = {
// Must be provided directly on each section.
items: ?Array<Item>,
key: string,
// Optional props will override list-wide props just for this section.
FooterComponent?: ?ReactClass<*>,
HeaderComponent?: ?ReactClass<*>,
ItemComponent?: ?ReactClass<{item: Item, index: number}>,
keyExtractor?: (item: Item) => string,
/**
* Called when the viewability of rows changes, as defined by the
* `viewablePercentThreshold` prop.
*/
onViewableItemsChanged?: ({viewableItems: Array<Viewable>, changed: Array<Viewable>}) => void,
}
type RequiredProps = {
sections: Array<Section>,
};
type OptionalProps = {
ItemComponent?: ?ReactClass<{item: Item, index: number}>,
SeparatorComponent?: ?ReactClass<*>,
/**
* Warning: Virtualization can drastically improve memory consumption for long lists, but trashes
* the state of items when they scroll out of the render window, so make sure all relavent data is
* stored outside of the recursive `ItemComponent` instance tree.
*/
enableVirtualization?: ?boolean,
keyExtractor?: (item: Item) => string,
onEndReached?: ({distanceFromEnd: number}) => void,
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: ?Function,
/**
* Called when the viewability of rows changes, as defined by the
* `viewablePercentThreshold` prop.
*/
onViewableItemsChanged?: ({viewableItems: Array<Viewable>, changed: Array<Viewable>}) => void,
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: boolean,
};
type Props = RequiredProps & OptionalProps;
// ####### VirtualizedList #######
type Item = any;
type ItemComponentType = ReactClass<{item: Item, index: number}>;
type RequiredProps = {
ItemComponent: ItemComponentType,
/**
* The default accessor functions assume this is an Array<{key: string}> but you can override
* getItem, getItemCount, and keyExtractor to handle any type of index-based data.
*/
data: any,
}
type OptionalProps = {
FooterComponent?: ?ReactClass<*>,
SeparatorComponent?: ?ReactClass<*>,
/**
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
* unmounts react instances that are outside of the render window. You should only need to disable
* this for debugging purposes.
*/
disableVirtualization: boolean,
getItem: (items: any, index: number) => ?Item,
getItemCount: (items: any) => number,
horizontal: boolean,
initialNumToRender: number,
keyExtractor: (item: Item, index: number) => string,
maxToRenderPerBatch: number,
onEndReached: ({distanceFromEnd: number}) => void,
onEndReachedThreshold: number, // units of visible length
onLayout?: ?Function,
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
*/
onRefresh?: ?Function,
/**
* Called when the viewability of rows changes, as defined by the
* `viewablePercentThreshold` prop.
*/
onViewableItemsChanged?: ({viewableItems: Array<Viewable>, changed: Array<Viewable>}) => void,
/**
* Set this true while waiting for new data from a refresh.
*/
refreshing?: boolean,
renderScrollComponent: (props: Object) => React.Element<*>,
shouldItemUpdate: (
props: {item: Item, index: number},
nextProps: {item: Item, index: number}
) => boolean,
updateCellsBatchingPeriod: number,
/**
* Percent of viewport that must be covered for a partially occluded item to count as
* "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
* that a single pixel in the viewport makes the item viewable, and a value of 100 means that
* an item must be either entirely visible or cover the entire viewport to count as viewable.
*/
viewablePercentThreshold: number,
windowSize: number, // units of visible length
};
type Props = RequiredProps & OptionalProps;
Hey @sahrens!
This is super exciting! I hope that "Reduced implementation complexity because no longer addressing Incremental rendering - assuming React Fiber will fix that problem" means we're at least somewhat close to integrating Fiber into React Native :) Everything in your proposal sounds nice to me, I responded to your open questions below, hopefully it's kind of helpful.
Open questions
I think this is valuable -- it can really trip people up when you use the array index for keys because if you remove an item then everything after it now has a new key.
I believe so, yes. Often useful eg: to know if first/last row, to zebra stripe, etc.
Do you have an example in mind where this would be useful? I can't think of any off of the top of my head.
Everything seems fine to me at first glance except Viewable which I'm a bit confused about.
A couple of thoughts:
imo
onLayout
seems ideal because it's how you'd write other components with React Native, but I don't feel strongly about this.No strong opinion about this either way, as long as the window size is customizable.
A third option might even be useful: how many screens tall should the window be. Awful name but let's say it's
windowSize
, sowindowSize={{ahead: 2, behind: 1}}
would render two screens of content ahead of the direction you're scrolling and 1 behind, or possiblywindowSize={4}
and have the listview determine how to distribute that. With pixels you probably will just multiply it by the device height anyhow.I think this is valuable, perhaps as third and forth (FlatListFixedHeight and SectionListFixedHeight) types of listviews to keep the other implementations clean.
Not familiar with the black magic required for this but I think as long as it's doable, jump-to is common enough that this would valuable.
If we pass a prop into rows to let them know that they are being predictively rendered, ideally we won't run into blank content because you can render a minimal placeholder item to ensure smooth scrolling. I think this
viewable
or is that something else?How would we prevent scrolling? If we keep momentum but freeze the main thread (:P), that would feel most like native. It's always weird when you hit the bottom of a list view in React Native and scrolling stops, because you don't know that you can scroll more.
Simple grid would be useful!
For pinterest style layout we'd need to aboslute position which seems pretty hard to deal with some edge cases eg: what happens when the height of one row grows? Also, do we support variable widths? It may be best to leave this to a third party lib, I suspect we'd run into a bunch of edge cases.
Minimal example / flow interface:
}
to closekey: 'a'
isViewable
is -- does this just mean that it's renderable?