diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index 186e93f6e3c..5ec67e27c92 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -25,6 +25,7 @@ import type { ViewabilityConfigCallbackPair, } from './ViewabilityHelper'; import type {RenderItemType, RenderItemProps} from './VirtualizedList'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; type RequiredProps = {| /** @@ -120,7 +121,7 @@ type OptionalProps = {| * 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: ItemT, index: number) => string, + keyExtractor?: ?(item: ItemT, index: number) => string, /** * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a * `flexWrap` layout. Items should all be the same height - masonry layouts are not supported. @@ -156,7 +157,6 @@ export type Props = { }; const defaultProps = { - ...VirtualizedList.defaultProps, numColumns: 1, /** * Enabling this prop on Android greatly improves scrolling performance with no known issues. @@ -503,20 +503,24 @@ class FlatList extends React.PureComponent, void> { }; _keyExtractor = (items: ItemT | Array, index: number) => { - const {keyExtractor, numColumns} = this.props; + const {numColumns} = this.props; + const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor; + if (numColumns > 1) { - invariant( - Array.isArray(items), - 'FlatList: Encountered internal consistency error, expected each item to consist of an ' + - 'array with 1-%s columns; instead, received a single item.', - numColumns, - ); - return ( - items - // $FlowFixMe[incompatible-call] - .map((it, kk) => keyExtractor(it, index * numColumns + kk)) - .join(':') - ); + if (Array.isArray(items)) { + return items + .map((item, kk) => + keyExtractor(((item: $FlowFixMe): ItemT), index * numColumns + kk), + ) + .join(':'); + } else { + invariant( + Array.isArray(items), + 'FlatList: Encountered internal consistency error, expected each item to consist of an ' + + 'array with 1-%s columns; instead, received a single item.', + numColumns, + ); + } } else { // $FlowFixMe Can't call keyExtractor with an array return keyExtractor(items, index); @@ -524,7 +528,8 @@ class FlatList extends React.PureComponent, void> { }; _pushMultiColumnViewable(arr: Array, v: ViewToken): void { - const {numColumns, keyExtractor} = this.props; + const {numColumns} = this.props; + const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor; v.item.forEach((item, ii) => { invariant(v.index != null, 'Missing index!'); const index = v.index * numColumns + ii; diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js index 90124df8e40..85687bb18f5 100644 --- a/Libraries/Lists/SectionList.js +++ b/Libraries/Lists/SectionList.js @@ -78,7 +78,7 @@ type OptionalProps> = {| * falls back to using the index, like react does. Note that this sets keys for each item, but * each overall section still needs its own key. */ - keyExtractor: (item: Item, index: number) => string, + keyExtractor?: ?(item: Item, index: number) => string, /** * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered * content. @@ -105,6 +105,10 @@ export type Props = {| VirtualizedSectionListProps, 'renderItem', >, + keyExtractor: $PropertyType< + VirtualizedSectionListProps, + 'keyExtractor', + >, ... }, >, @@ -113,7 +117,6 @@ export type Props = {| |}; const defaultProps = { - ...VirtualizedSectionList.defaultProps, stickySectionHeadersEnabled: Platform.OS === 'ios', }; diff --git a/Libraries/Lists/VirtualizeUtils.js b/Libraries/Lists/VirtualizeUtils.js index 126839d3f92..1d80b378aa8 100644 --- a/Libraries/Lists/VirtualizeUtils.js +++ b/Libraries/Lists/VirtualizeUtils.js @@ -233,3 +233,13 @@ export function computeWindowedRenderLimits( } return {first, last}; } + +export function keyExtractor(item: any, index: number): string { + if (typeof item === 'object' && item?.key != null) { + return item.key; + } + if (typeof item === 'object' && item?.id != null) { + return item.id; + } + return String(index); +} diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index ae53b7f85d7..b23c3b6921a 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -22,7 +22,10 @@ const infoLog = require('../Utilities/infoLog'); const invariant = require('invariant'); import VirtualizedListInjection from './VirtualizedListInjection'; -import {computeWindowedRenderLimits} from './VirtualizeUtils'; +import { + keyExtractor as defaultKeyExtractor, + computeWindowedRenderLimits, +} from './VirtualizeUtils'; import * as React from 'react'; import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; @@ -132,7 +135,7 @@ type OptionalProps = {| * Reverses the direction of scroll. Uses scale transforms of -1. */ inverted?: ?boolean, - keyExtractor: (item: Item, index: number) => string, + keyExtractor?: ?(item: Item, index: number) => string, /** * Each cell is rendered using this element. Can be a React Component Class, * or a render function. Defaults to using View. @@ -303,10 +306,6 @@ type Props = {| ...OptionalProps, |}; -type DefaultProps = {| - keyExtractor: (item: Item, index: number) => string, -|}; - let _usedIndexForKey = false; let _keylessItemComponentName: string = ''; @@ -315,26 +314,37 @@ type State = { last: number, }; +/** + * Default Props Helper Functions + * Use the following helper functions for default values + */ + +// horizontalOrDefault(this.props.horizontal) function horizontalOrDefault(horizontal: ?boolean) { return horizontal ?? false; } +// initialNumToRenderOrDefault(this.props.initialNumToRenderOrDefault) function initialNumToRenderOrDefault(initialNumToRender: ?number) { return initialNumToRender ?? 10; } +// maxToRenderPerBatchOrDefault(this.props.maxToRenderPerBatch) function maxToRenderPerBatchOrDefault(maxToRenderPerBatch: ?number) { return maxToRenderPerBatch ?? 10; } +// onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold) function onEndReachedThresholdOrDefault(onEndReachedThreshold: ?number) { return onEndReachedThreshold ?? 2; } +// scrollEventThrottleOrDefault(this.props.scrollEventThrottle) function scrollEventThrottleOrDefault(scrollEventThrottle: ?number) { return scrollEventThrottle ?? 50; } +// windowSizeOrDefault(this.props.windowSize) function windowSizeOrDefault(windowSize: ?number) { return windowSize ?? 21; } @@ -365,6 +375,7 @@ function windowSizeOrDefault(windowSize: ?number) { * and we are working on improving it behind the scenes. * - By default, the list looks for a `key` or `id` prop on each item and uses that for the React key. * Alternatively, you can provide a custom `keyExtractor` prop. + * - As an effort to remove defaultProps, use helper functions when referencing certain props * */ class VirtualizedList extends React.PureComponent { @@ -580,22 +591,6 @@ class VirtualizedList extends React.PureComponent { } } - static defaultProps: DefaultProps = { - keyExtractor: (item: Item, index: number) => { - if (item.key != null) { - return item.key; - } - if (item.id != null) { - return item.id; - } - _usedIndexForKey = true; - if (item.type && item.type.displayName) { - _keylessItemComponentName = item.type.displayName; - } - return String(index); - }, - }; - _getCellKey(): string { return this.context?.cellKey || 'rootList'; } @@ -804,7 +799,6 @@ class VirtualizedList extends React.PureComponent { getItem, getItemCount, horizontal, - keyExtractor, } = this.props; const stickyOffset = this.props.ListHeaderComponent ? 1 : 0; const end = getItemCount(data) - 1; @@ -812,7 +806,7 @@ class VirtualizedList extends React.PureComponent { last = Math.min(end, last); for (let ii = first; ii <= last; ii++) { const item = getItem(data, ii); - const key = keyExtractor(item, ii); + const key = this._keyExtractor(item, ii); this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { stickyHeaderIndices.push(cells.length); @@ -864,6 +858,21 @@ class VirtualizedList extends React.PureComponent { _getSpacerKey = (isVertical: boolean): string => isVertical ? 'height' : 'width'; + _keyExtractor(item: Item, index: number) { + if (this.props.keyExtractor != null) { + return this.props.keyExtractor(item, index); + } + + const key = defaultKeyExtractor(item, index); + if (key === String(index)) { + _usedIndexForKey = true; + if (item.type && item.type.displayName) { + _keylessItemComponentName = item.type.displayName; + } + } + return key; + } + render(): React.Node { if (__DEV__) { const flatStyles = flattenStyle(this.props.contentContainerStyle); @@ -1816,9 +1825,9 @@ class VirtualizedList extends React.PureComponent { }; _createViewToken = (index: number, isViewable: boolean) => { - const {data, getItem, keyExtractor} = this.props; + const {data, getItem} = this.props; const item = getItem(data, index); - return {index, item, key: keyExtractor(item, index), isViewable}; + return {index, item, key: this._keyExtractor(item, index), isViewable}; }; _getFrameMetricsApprox = ( @@ -1854,19 +1863,13 @@ class VirtualizedList extends React.PureComponent { inLayout?: boolean, ... } => { - const { - data, - getItem, - getItemCount, - getItemLayout, - keyExtractor, - } = this.props; + const {data, getItem, getItemCount, getItemLayout} = this.props; invariant( getItemCount(data) > index, 'Tried to get frame for out of range index ' + index, ); const item = getItem(data, index); - let frame = item && this._frames[keyExtractor(item, index)]; + let frame = item && this._frames[this._keyExtractor(item, index)]; if (!frame || frame.index !== index) { if (getItemLayout) { frame = getItemLayout(data, index); diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index af85f52d351..8c621a8468e 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -17,6 +17,7 @@ const VirtualizedList = require('./VirtualizedList'); const invariant = require('invariant'); import type {ViewToken} from './ViewabilityHelper'; +import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; type Item = any; @@ -99,14 +100,18 @@ type OptionalProps> = {| onEndReached?: ?({distanceFromEnd: number, ...}) => void, |}; -type VirtualizedListProps = React.ElementProps; +type VirtualizedListProps = React.ElementConfig; export type Props = {| ...RequiredProps, ...OptionalProps, ...$Diff< VirtualizedListProps, - {renderItem: $PropertyType, ...}, + { + renderItem: $PropertyType, + data: $PropertyType, + ... + }, >, |}; export type ScrollToLocationParamsType = {| @@ -117,11 +122,6 @@ export type ScrollToLocationParamsType = {| viewPosition?: number, |}; -type DefaultProps = {| - ...typeof VirtualizedList.defaultProps, - data: $ReadOnlyArray, -|}; - type State = {childProps: VirtualizedListProps, ...}; /** @@ -132,11 +132,6 @@ type State = {childProps: VirtualizedListProps, ...}; class VirtualizedSectionList< SectionT: SectionBase, > extends React.PureComponent, State> { - static defaultProps: DefaultProps = { - ...VirtualizedList.defaultProps, - data: [], - }; - scrollToLocation(params: ScrollToLocationParamsType) { let index = params.itemIndex; for (let i = 0; i < params.sectionIndex; i++) { @@ -217,11 +212,11 @@ class VirtualizedSectionList< ); } - _getItem = ( + _getItem( props: Props, sections: ?$ReadOnlyArray, index: number, - ): ?Item => { + ): ?Item { if (!sections) { return null; } @@ -243,7 +238,7 @@ class VirtualizedSectionList< } } return null; - }; + } _keyExtractor = (item: Item, index: number) => { const info = this._subExtractor(index); @@ -292,7 +287,8 @@ class VirtualizedSectionList< trailingSection: sections[i + 1], }; } else { - const extractor = section.keyExtractor || keyExtractor; + const extractor = + section.keyExtractor || keyExtractor || defaultKeyExtractor; return { section, key: @@ -313,14 +309,18 @@ class VirtualizedSectionList< if (!info) { return null; } - const keyExtractor = info.section.keyExtractor || this.props.keyExtractor; + const keyExtractorWithNullableIndex = info.section.keyExtractor; + const keyExtractorWithNonNullableIndex = + this.props.keyExtractor || defaultKeyExtractor; + const key = + keyExtractorWithNullableIndex != null + ? keyExtractorWithNullableIndex(viewable.item, info.index) + : keyExtractorWithNonNullableIndex(viewable.item, info.index ?? 0); + return { ...viewable, index: info.index, - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.63 was deployed. To see the error delete this - * comment and run Flow. */ - key: keyExtractor(viewable.item, info.index), + key, section: info.section, }; }; @@ -581,4 +581,11 @@ class ItemWithSeparator extends React.Component< } } -module.exports = VirtualizedSectionList; +module.exports = (VirtualizedSectionList: React.AbstractComponent< + React.ElementConfig, + $ReadOnly<{ + getListRef: () => ?React.ElementRef, + scrollToLocation: (params: ScrollToLocationParamsType) => void, + ... + }>, +>); diff --git a/Libraries/Lists/__tests__/VirtualizedSectionList-test.js b/Libraries/Lists/__tests__/VirtualizedSectionList-test.js index 8a13d7556b4..260da5555f7 100644 --- a/Libraries/Lists/__tests__/VirtualizedSectionList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedSectionList-test.js @@ -103,26 +103,37 @@ describe('VirtualizedSectionList', () => { it('handles separators correctly', () => { const infos = []; - const component = ReactTestRenderer.create( - } - sections={[ - {title: 's0', data: [{key: 'i0'}, {key: 'i1'}, {key: 'i2'}]}, - ]} - renderItem={info => { - infos.push(info); - return ; - }} - getItem={(data, key) => data[key]} - getItemCount={data => data.length} - />, - ); + let component; + ReactTestRenderer.act(() => { + component = ReactTestRenderer.create( + } + sections={[ + {title: 's0', data: [{key: 'i0'}, {key: 'i1'}, {key: 'i2'}]}, + ]} + renderItem={info => { + infos.push(info); + return ; + }} + getItem={(data, key) => data[key]} + getItemCount={data => data.length} + />, + ); + }); expect(component).toMatchSnapshot(); - infos[1].separators.highlight(); + + ReactTestRenderer.act(() => { + infos[1].separators.highlight(); + }); expect(component).toMatchSnapshot(); - infos[2].separators.updateProps('leading', {press: true}); + ReactTestRenderer.act(() => { + infos[2].separators.updateProps('leading', {press: true}); + }); + expect(component).toMatchSnapshot(); + ReactTestRenderer.act(() => { + infos[1].separators.unhighlight(); + }); expect(component).toMatchSnapshot(); - infos[1].separators.unhighlight(); }); it('handles nested lists', () => { diff --git a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap index 9763a8bd8b5..6bd99586b38 100644 --- a/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap @@ -44,7 +44,6 @@ exports[`VirtualizedList forwards correct stickyHeaderIndices when all in initia getItemCount={[Function]} getItemLayout={[Function]} initialNumToRender={10} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -186,7 +185,6 @@ exports[`VirtualizedList forwards correct stickyHeaderIndices when partially in getItemCount={[Function]} getItemLayout={[Function]} initialNumToRender={5} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -266,7 +264,6 @@ exports[`VirtualizedList handles nested lists 1`] = ` } getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -297,7 +294,6 @@ exports[`VirtualizedList handles nested lists 1`] = ` getItem={[Function]} getItemCount={[Function]} horizontal={false} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -345,7 +341,6 @@ exports[`VirtualizedList handles nested lists 1`] = ` getItem={[Function]} getItemCount={[Function]} horizontal={true} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -413,7 +408,6 @@ exports[`VirtualizedList handles separators correctly 1`] = ` } getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -488,7 +482,6 @@ exports[`VirtualizedList handles separators correctly 2`] = ` } getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -563,7 +556,6 @@ exports[`VirtualizedList handles separators correctly 3`] = ` } getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -711,7 +703,6 @@ exports[`VirtualizedList keeps sticky headers realized after scrolled out of vie getItemCount={[Function]} getItemLayout={[Function]} initialNumToRender={1} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -900,7 +891,6 @@ exports[`VirtualizedList realizes sticky headers in viewport on batched render 1 getItemCount={[Function]} getItemLayout={[Function]} initialNumToRender={1} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -1172,7 +1162,6 @@ exports[`VirtualizedList renders empty list 1`] = ` data={Array []} getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -1196,7 +1185,6 @@ exports[`VirtualizedList renders empty list with empty component 1`] = ` data={Array []} getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -1236,7 +1224,6 @@ exports[`VirtualizedList renders list with empty component 1`] = ` } getItem={[Function]} getItemCount={[Function]} - keyExtractor={[Function]} onContentSizeChange={[Function]} onLayout={[Function]} onMomentumScrollBegin={[Function]} @@ -1265,7 +1252,6 @@ exports[`VirtualizedList renders null list 1`] = ` `; +exports[`VirtualizedSectionList handles separators correctly 4`] = ` + + + + + + + + + + + + + + + + + + + + + +`; + exports[`VirtualizedSectionList renders all the bells and whistles 1`] = `