Breaking API change - abandon ItemComponent in favor of renderItem

Summary:
After a fair bit of use, we have concluded that the `ItemComponent` mechanism is not worth the
hassle. Flow has trouble type checking it thoroughly, requiring an 'item' prop is annoying, and it
is very common to need to capture `this` anyway, e.g. for an `onPress` handler. A common pattern was
something like:

  _renderItem = ({item}) => <MyItem foo={item.foo} onPress={() => this._onPress(item)} />};
  ...
  ItemComponent={this._renderItem}

which wouldn't flow check the props and doesn't benefit from reusing components.

If we find some specific patterns that would benefit from the `ItemComponent` pattern, we can create
a new component that provides that API and wraps `FlatList` under the hood.

I'm going to do `SectionList` in a stacked diff.

Reviewed By: bvaughn

Differential Revision: D4625338

fbshipit-source-id: a4901f1c9d77e0115b0b8032b8c210f624e97ea3
This commit is contained in:
Spencer Ahrens 2017-02-28 02:09:09 -08:00 committed by Facebook Github Bot
parent f7d1060418
commit 2a1ab36257
11 changed files with 154 additions and 143 deletions

View File

@ -108,7 +108,6 @@ class FlatListExample extends React.PureComponent {
<FlatList
HeaderComponent={HeaderComponent}
FooterComponent={FooterComponent}
ItemComponent={this._renderItemComponent}
SeparatorComponent={SeparatorComponent}
data={filteredData}
debug={this.state.debug}
@ -122,6 +121,7 @@ class FlatListExample extends React.PureComponent {
onViewableItemsChanged={this._onViewableItemsChanged}
ref={this._captureRef}
refreshing={false}
renderItem={this._renderItemComponent}
shouldItemUpdate={this._shouldItemUpdate}
viewabilityConfig={VIEWABILITY_CONFIG}
/>

View File

@ -86,22 +86,16 @@ class ItemComponent extends React.PureComponent {
}
}
class StackedItemComponent extends React.PureComponent {
props: {
item: Item,
};
render() {
const {item} = this.props;
const itemHash = Math.abs(hashCode(item.title));
const imgSource = THUMB_URLS[itemHash % THUMB_URLS.length];
return (
<View style={styles.stacked}>
<Text style={styles.stackedText}>{item.title} - {item.text}</Text>
<Image style={styles.thumb} source={imgSource} />
</View>
);
}
}
const renderStackedItem = ({item}: {item: Item}) => {
const itemHash = Math.abs(hashCode(item.title));
const imgSource = THUMB_URLS[itemHash % THUMB_URLS.length];
return (
<View style={styles.stacked}>
<Text style={styles.stackedText}>{item.title} - {item.text}</Text>
<Image style={styles.thumb} source={imgSource} />
</View>
);
};
class FooterComponent extends React.PureComponent {
render() {
@ -287,9 +281,9 @@ module.exports = {
ItemComponent,
PlainInput,
SeparatorComponent,
StackedItemComponent,
genItemData,
getItemLayout,
pressItem,
renderSmallSwitchOption,
renderStackedItem,
};

View File

@ -99,7 +99,6 @@ class MultiColumnExample extends React.PureComponent {
<FlatList
FooterComponent={FooterComponent}
HeaderComponent={HeaderComponent}
ItemComponent={this._renderItemComponent}
SeparatorComponent={SeparatorComponent}
getItemLayout={this.state.fixedHeight ? this._getItemLayout : undefined}
data={filteredData}
@ -107,6 +106,7 @@ class MultiColumnExample extends React.PureComponent {
numColumns={this.state.numColumns || 1}
onRefresh={() => alert('onRefresh: nothing to refresh :P')}
refreshing={false}
renderItem={this._renderItemComponent}
shouldItemUpdate={this._shouldItemUpdate}
disableVirtualization={!this.state.virtualized}
onViewableItemsChanged={this._onViewableItemsChanged}

View File

@ -42,10 +42,10 @@ const {
ItemComponent,
PlainInput,
SeparatorComponent,
StackedItemComponent,
genItemData,
pressItem,
renderSmallSwitchOption,
renderStackedItem,
} = require('./ListExampleShared');
const VIEWABILITY_CONFIG = {
@ -54,7 +54,7 @@ const VIEWABILITY_CONFIG = {
waitForInteraction: true,
};
const SectionHeaderComponent = ({section}) => (
const renderSectionHeader = ({section}) => (
<View>
<Text style={styles.headerText}>SECTION HEADER: {section.key}</Text>
<SeparatorComponent />
@ -104,16 +104,16 @@ class SectionListExample extends React.PureComponent {
<SectionList
ListHeaderComponent={HeaderComponent}
ListFooterComponent={FooterComponent}
ItemComponent={this._renderItemComponent}
SectionHeaderComponent={SectionHeaderComponent}
SectionSeparatorComponent={() => <CustomSeparatorComponent text="SECTION SEPARATOR" />}
ItemSeparatorComponent={() => <CustomSeparatorComponent text="ITEM SEPARATOR" />}
enableVirtualization={this.state.virtualized}
onRefresh={() => alert('onRefresh: nothing to refresh :P')}
onViewableItemsChanged={this._onViewableItemsChanged}
refreshing={false}
renderItem={this._renderItemComponent}
renderSectionHeader={renderSectionHeader}
sections={[
{ItemComponent: StackedItemComponent, key: 's1', data: [
{renderItem: renderStackedItem, key: 's1', data: [
{title: 'Item In Header Section', text: 'Section s1', key: '0'},
]},
{key: 's2', data: [

View File

@ -43,14 +43,21 @@ import type {StyleObj} from 'StyleSheetTypes';
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
import type {Props as VirtualizedListProps} from 'VirtualizedList';
type Item = any;
type RequiredProps<ItemT> = {
/**
* Note this can be a normal class component, or a functional component, such as a render method
* on your main component.
* Takes an item from `data` and renders it into the list. Typicaly usage:
*
* _renderItem = ({item}) => (
* <TouchableOpacity onPress={() => this._onPress(item)}>
* <Text>{item.title}}</Text>
* <TouchableOpacity/>
* );
* ...
* <FlatList data={[{title: 'Title Text'}]} renderItem={this._renderItem} />
*
* Provides additional metadata like `index` if you need it.
*/
ItemComponent: ReactClass<{item: ItemT, index: number}>,
renderItem: ({item: ItemT, index: number}) => ?React.Element<*>,
/**
* For simplicity, data is just a plain array. If you want to use something else, like an
* immutable list, use the underlying `VirtualizedList` directly.
@ -125,8 +132,8 @@ type OptionalProps<ItemT> = {
* Optional optimization to minimize re-rendering items.
*/
shouldItemUpdate: (
prevProps: {item: ItemT, index: number},
nextProps: {item: ItemT, index: number}
prevInfo: {item: ItemT, index: number},
nextInfo: {item: ItemT, index: number}
) => boolean,
/**
* See ViewabilityHelper for flow type and comments.
@ -159,7 +166,7 @@ type DefaultProps = typeof defaultProps;
*
* <FlatList
* data={[{key: 'a', {key: 'b'}]}
* ItemComponent={({item}) => <Text>{item.key}</Text>}
* renderItem={({item}) => <Text>{item.key}</Text>}
* />
*/
class FlatList<ItemT> extends React.PureComponent<DefaultProps, Props<ItemT>, void> {
@ -186,7 +193,7 @@ class FlatList<ItemT> extends React.PureComponent<DefaultProps, Props<ItemT>, vo
* Requires linear scan through data - use scrollToIndex instead if possible. May be janky without
* `getItemLayout` prop.
*/
scrollToItem(params: {animated?: ?boolean, item: Item, viewPosition?: number}) {
scrollToItem(params: {animated?: ?boolean, item: ItemT, viewPosition?: number}) {
this._listRef.scrollToItem(params);
}
@ -305,18 +312,21 @@ class FlatList<ItemT> extends React.PureComponent<DefaultProps, Props<ItemT>, vo
}
};
_renderItem = ({item, index}) => {
const {ItemComponent, numColumns, columnWrapperStyle} = this.props;
_renderItem = (info: {item: ItemT | Array<ItemT>, index: number}) => {
const {renderItem, numColumns, columnWrapperStyle} = this.props;
if (numColumns > 1) {
const {item, index} = info;
invariant(Array.isArray(item), 'Expected array of items with numColumns > 1');
return (
<View style={[{flexDirection: 'row'}, columnWrapperStyle]}>
{item.map((it, kk) =>
<ItemComponent key={kk} item={it} index={index * numColumns + kk} />)
}
{item.map((it, kk) => {
const element = renderItem({item: it, index: index * numColumns + kk});
return element && React.cloneElement(element, {key: kk});
})}
</View>
);
} else {
return <ItemComponent item={item} index={index} />;
return renderItem(info);
}
};
@ -340,7 +350,7 @@ class FlatList<ItemT> extends React.PureComponent<DefaultProps, Props<ItemT>, vo
return (
<VirtualizedList
{...this.props}
ItemComponent={this._renderItem}
renderItem={this._renderItem}
getItem={this._getItem}
getItemCount={this._getItemCount}
keyExtractor={this._keyExtractor}

View File

@ -43,9 +43,9 @@ type Item = any;
type NormalProps = {
FooterComponent?: ReactClass<*>,
ItemComponent: ReactClass<{item: Item, index: number}>,
SectionHeaderComponent?: ReactClass<{info: Object}>,
SeparatorComponent?: ReactClass<*>, // not supported yet
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
renderSectionHeader?: ({section: Object}) => ?React.Element<*>,
SeparatorComponent?: ?ReactClass<*>, // not supported yet
// Provide either `items` or `sections`
items?: ?Array<Item>, // By default, an Item is assumed to be {key: string}
@ -163,13 +163,12 @@ class MetroListView extends React.Component {
}
_renderFooter = () => <this.props.FooterComponent key="$footer" />;
_renderRow = (item, sectionID, rowID, highlightRow) => {
const {ItemComponent} = this.props;
return <ItemComponent item={item} index={rowID} />;
return this.props.renderItem({item, index: rowID});
};
_renderSectionHeader = (section, sectionID) => {
const {SectionHeaderComponent} = this.props;
invariant(SectionHeaderComponent, 'Must provide SectionHeaderComponent with sections prop');
return <SectionHeaderComponent section={section} />;
const {renderSectionHeader} = this.props;
invariant(renderSectionHeader, 'Must provide renderSectionHeader with sections prop');
return renderSectionHeader({section});
}
_renderSeparator = (sID, rID) => <this.props.SeparatorComponent key={sID + rID} />;
}

View File

@ -47,7 +47,7 @@ type SectionBase<SectionItemT> = {
key: string,
// Optional props will override list-wide props just for this section.
ItemComponent?: ?ReactClass<{item: SectionItemT, index: number}>,
renderItem?: ?({item: SectionItemT, index: number}) => ?React.Element<*>,
SeparatorComponent?: ?ReactClass<*>,
keyExtractor?: (item: SectionItemT) => string,
@ -55,10 +55,6 @@ type SectionBase<SectionItemT> = {
// FooterComponent?: ?ReactClass<*>,
// HeaderComponent?: ?ReactClass<*>,
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
// TODO: support recursive sections
// SectionHeaderComponent?: ?ReactClass<{section: SectionBase<*>}>,
// sections?: ?Array<Section>;
};
type RequiredProps<SectionT: SectionBase<*>> = {
@ -67,9 +63,9 @@ type RequiredProps<SectionT: SectionBase<*>> = {
type OptionalProps<SectionT: SectionBase<*>> = {
/**
* Default renderer for every item in every section.
* Default renderer for every item in every section. Can be over-ridden on a per-section basis.
*/
ItemComponent: ReactClass<{item: Item, index: number}>,
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
/**
* Rendered in between adjacent Items within each section.
*/
@ -85,7 +81,7 @@ type OptionalProps<SectionT: SectionBase<*>> = {
/**
* Rendered at the top of each section. Sticky headers are not yet supported.
*/
SectionHeaderComponent?: ?ReactClass<{section: SectionT}>,
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
/**
* Rendered in between each section.
*/
@ -93,7 +89,7 @@ type OptionalProps<SectionT: SectionBase<*>> = {
/**
* 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.
* stored outside of the recursive `renderItem` instance tree.
*/
enableVirtualization?: ?boolean,
keyExtractor: (item: Item, index: number) => string,

View File

@ -47,7 +47,7 @@ const {computeWindowedRenderLimits} = require('VirtualizeUtils');
import type {ViewabilityConfig, ViewToken} from 'ViewabilityHelper';
type Item = any;
type ItemComponentType = ReactClass<{item: Item, index: number}>;
type renderItemType = ({item: Item, index: number}) => ?React.Element<*>;
/**
* Renders a virtual list of items given a data blob and accessor functions. Items that are outside
@ -63,7 +63,7 @@ type ItemComponentType = ReactClass<{item: Item, index: number}>;
*
*/
type RequiredProps = {
ItemComponent: ItemComponentType,
renderItem: renderItemType,
/**
* 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.
@ -633,7 +633,7 @@ class CellRenderer extends React.Component {
onLayout: (event: Object, cellKey: string, index: number) => void,
onUnmount: (cellKey: string) => void,
parentProps: {
ItemComponent: ItemComponentType,
renderItem: renderItemType,
getItemLayout?: ?Function,
shouldItemUpdate: (
props: {item: Item, index: number},
@ -654,8 +654,9 @@ class CellRenderer extends React.Component {
}
render() {
const {item, index, parentProps} = this.props;
const {ItemComponent, getItemLayout} = parentProps;
const element = <ItemComponent item={item} index={index} />;
const {renderItem, getItemLayout} = parentProps;
invariant(renderItem, 'no renderItem!');
const element = renderItem({item, index});
if (getItemLayout && !parentProps.debug) {
return element;
}

View File

@ -51,7 +51,7 @@ type SectionBase = {
key: string,
// Optional props will override list-wide props just for this section.
ItemComponent?: ?ReactClass<{item: SectionItem, index: number}>,
renderItem?: ?({item: SectionItem, index: number}) => ?React.Element<*>,
SeparatorComponent?: ?ReactClass<*>,
keyExtractor?: (item: SectionItem) => string,
@ -59,10 +59,6 @@ type SectionBase = {
// FooterComponent?: ?ReactClass<*>,
// HeaderComponent?: ?ReactClass<*>,
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
// TODO: support recursive sections
// SectionHeaderComponent?: ?ReactClass<{section: SectionBase}>,
// sections?: ?Array<Section>;
};
type RequiredProps<SectionT: SectionBase> = {
@ -73,15 +69,19 @@ type OptionalProps<SectionT: SectionBase> = {
/**
* Rendered after the last item in the last section.
*/
FooterComponent?: ?ReactClass<*>,
ListFooterComponent?: ?ReactClass<*>,
/**
* Rendered at the very beginning of the list.
*/
ListHeaderComponent?: ?ReactClass<*>,
/**
* Default renderer for every item in every section.
*/
ItemComponent: ReactClass<{item: Item, index: number}>,
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
/**
* Rendered at the top of each section. In the future, a sticky option will be added.
*/
SectionHeaderComponent?: ?ReactClass<{section: SectionT}>,
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
/**
* Rendered at the bottom of every Section, except the very last one, in place of the normal
* SeparatorComponent.
@ -90,11 +90,11 @@ type OptionalProps<SectionT: SectionBase> = {
/**
* Rendered at the bottom of every Item except the very last one in the last section.
*/
SeparatorComponent?: ?ReactClass<*>,
ItemSeparatorComponent?: ?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.
* stored outside of the recursive `renderItem` instance tree.
*/
enableVirtualization?: ?boolean,
keyExtractor: (item: Item, index: number) => string,
@ -220,14 +220,16 @@ class VirtualizedSectionList<SectionT: SectionBase>
if (!info) {
return null;
} else if (info.index == null) {
const {SectionHeaderComponent} = this.props;
return SectionHeaderComponent ? <SectionHeaderComponent section={info.section} /> : null;
const {renderSectionHeader} = this.props;
return renderSectionHeader ? renderSectionHeader({section: info.section}) : null;
} else {
const ItemComponent = info.section.ItemComponent || this.props.ItemComponent;
const renderItem = info.section.renderItem ||
this.props.renderItem;
const SeparatorComponent = this._getSeparatorComponent(index, info);
invariant(renderItem, 'no renderItem!');
return (
<View>
<ItemComponent item={item} index={info.index} />
{renderItem({item, index: info.index || 0})}
{SeparatorComponent && <SeparatorComponent />}
</View>
);
@ -239,7 +241,7 @@ class VirtualizedSectionList<SectionT: SectionBase>
if (!info) {
return null;
}
const SeparatorComponent = info.section.SeparatorComponent || this.props.SeparatorComponent;
const SeparatorComponent = info.section.SeparatorComponent || this.props.ItemSeparatorComponent;
const {SectionSeparatorComponent} = this.props;
const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
const isLastItemInSection = info.index === info.section.data.length - 1;
@ -265,8 +267,10 @@ class VirtualizedSectionList<SectionT: SectionBase>
return {
childProps: {
...props,
ItemComponent: this._renderItem,
SeparatorComponent: undefined, // Rendered with ItemComponent
FooterComponent: this.props.ListFooterComponent,
HeaderComponent: this.props.ListHeaderComponent,
renderItem: this._renderItem,
SeparatorComponent: undefined, // Rendered with renderItem
data: props.sections,
getItemCount: () => itemCount,
getItem,

View File

@ -14,64 +14,77 @@
const FlatList = require('FlatList');
const React = require('react');
class MyListItem extends React.Component {
props: {
item: {
title: string,
},
};
render() {
return <span />;
}
function renderMyListItem(info: {item: {title: string}, index: number}) {
return <span />;
}
module.exports = {
testBadDataWithTypicalItemComponent(): React.Element<*> {
testEverythingIsFine() {
const data = [{
title: 'Title Text',
key: 1,
}];
return <FlatList renderItem={renderMyListItem} data={data} />;
},
testBadDataWithTypicalItem() {
// $FlowExpectedError - bad title type 6, should be string
const data = [{
title: 6,
key: 1,
}];
return <FlatList ItemComponent={MyListItem} data={data} />;
return <FlatList renderItem={renderMyListItem} data={data} />;
},
testMissingFieldWithTypicalItemComponent(): React.Element<*> {
testMissingFieldWithTypicalItem() {
const data = [{
key: 1,
}];
// $FlowExpectedError - missing title
return <FlatList ItemComponent={MyListItem} data={data} />;
return <FlatList renderItem={renderMyListItem} data={data} />;
},
testGoodDataWithGoodCustomItemComponentFunction() {
testGoodDataWithBadCustomRenderItemFunction() {
const data = [{
widgetCount: 3,
widget: 6,
key: 1,
}];
return (
<FlatList
ItemComponent={(props: {widgetCount: number}): React.Element<*> =>
<MyListItem item={{title: props.widgetCount + ' Widgets'}} />
renderItem={(info) =>
// $FlowExpectedError - bad widgetCount type 6, should be Object
<span>{info.item.widget.missingProp}</span>
}
data={data}
/>
);
},
testBadNonInheritedDefaultProp(): React.Element<*> {
const data = [];
// $FlowExpectedError - bad numColumns type "lots"
return <FlatList ItemComponent={MyListItem} data={data} numColumns="lots" />;
testBadRenderItemFunction() {
const data = [{
title: 'foo',
key: 1,
}];
return [
// $FlowExpectedError - title should be inside `item`
<FlatList renderItem={(info: {title: string}) => <span /> } data={data} />,
// $FlowExpectedError - bad index type string, should be number
<FlatList renderItem={(info: {item: any, index: string}) => <span /> } data={data} />,
// $FlowExpectedError - bad title type number, should be string
<FlatList renderItem={(info: {item: {title: number}}) => <span /> } data={data} />,
// EverythingIsFine
<FlatList renderItem={(info: {item: {title: string}}) => <span /> } data={data} />,
];
},
testBadInheritedDefaultProp(): React.Element<*> {
const data = [];
// $FlowExpectedError - bad windowSize type "big"
return <FlatList ItemComponent={MyListItem} data={data} windowSize="big" />;
},
testMissingData(): React.Element<*> {
// $FlowExpectedError - missing `data` prop
return <FlatList ItemComponent={MyListItem} />;
testOtherBadProps() {
return [
// $FlowExpectedError - bad numColumns type "lots"
<FlatList renderItem={renderMyListItem} data={[]} numColumns="lots" />,
// $FlowExpectedError - bad windowSize type "big"
<FlatList renderItem={renderMyListItem} data={[]} windowSize="big" />,
// $FlowExpectedError - missing `data` prop
<FlatList renderItem={renderMyListItem} />,
];
},
};

View File

@ -14,55 +14,49 @@
const React = require('react');
const SectionList = require('SectionList');
class MyListItem extends React.Component {
props: {
item: {
title: string,
},
};
render() {
return <span />;
}
function renderMyListItem(info: {item: {title: string}, index: number}) {
return <span />;
}
class MyHeader extends React.Component {
props: {
section: {
fooNumber: number,
}
};
render() {
return <span />;
}
}
const renderMyHeader = ({section}: {section: {fooNumber: number} & Object}) => <span />;
module.exports = {
testGoodDataWithGoodCustomItemComponentFunction() {
testGoodDataWithGoodItem() {
const sections = [{
key: 'a', data: [{
widgetCount: 3,
title: 'foo',
key: 1,
}],
}];
return (
<SectionList
ItemComponent={(props: {widgetCount: number}): React.Element<*> =>
<MyListItem item={{title: props.widgetCount + ' Widgets'}} />
}
sections={sections}
/>
);
return <SectionList renderItem={renderMyListItem} sections={sections} />;
},
testBadRenderItemFunction() {
const sections = [{
key: 'a', data: [{
title: 'foo',
key: 1,
}],
}];
return [
// $FlowExpectedError - title should be inside `item`
<SectionList renderItem={(info: {title: string}) => <span /> } sections={sections} />,
// $FlowExpectedError - bad index type string, should be number
<SectionList renderItem={(info: {index: string}) => <span /> } sections={sections} />,
// EverythingIsFine
<SectionList renderItem={(info: {item: {title: string}}) => <span /> } sections={sections} />,
];
},
testBadInheritedDefaultProp(): React.Element<*> {
const sections = [];
// $FlowExpectedError - bad windowSize type "big"
return <SectionList ItemComponent={MyListItem} sections={sections} windowSize="big" />;
return <SectionList renderItem={renderMyListItem} sections={sections} windowSize="big" />;
},
testMissingData(): React.Element<*> {
// $FlowExpectedError - missing `sections` prop
return <SectionList ItemComponent={MyListItem} />;
return <SectionList renderItem={renderMyListItem} />;
},
testBadSectionsShape(): React.Element<*> {
@ -73,7 +67,7 @@ module.exports = {
}],
}];
// $FlowExpectedError - section missing `data` field
return <SectionList ItemComponent={MyListItem} sections={sections} />;
return <SectionList renderItem={renderMyListItem} sections={sections} />;
},
testBadSectionsMetadata(): React.Element<*> {
@ -86,8 +80,8 @@ module.exports = {
}];
return (
<SectionList
SectionHeaderComponent={MyHeader}
ItemComponent={MyListItem}
renderSectionHeader={renderMyHeader}
renderItem={renderMyListItem}
sections={sections}
/>
);