Skip to content

Instantly share code, notes, and snippets.

@sahrens
Last active May 22, 2020 08:46
Show Gist options
  • Save sahrens/902d49c6c154cd09fafc52a79503728f to your computer and use it in GitHub Desktop.
Save sahrens/902d49c6c154cd09fafc52a79503728f to your computer and use it in GitHub Desktop.

A Better ListView for React Native

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 and SectionList. 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 optional keyExtractor 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 an items array. No more required rowHasChanged function.

  • Built-in PTR - just set the onRefresh and refreshing props.

  • *Component props instead of render* 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 different ItemComponents, 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 to ItemComponents?
  • 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 use RCTScrollViewManager.calculateChildFrames like ListView? I have a perf optimization that should get onLayout 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;
@ide
Copy link

ide commented Jan 28, 2017

About nested sections -- would this be rendered differently than simply flattening the sections before passing them into the SectionList? If it's not necessary or especially helpful for SectionList to support this I'd be in favor of delegating section-flattening to user space or a separate npm package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment