Skip to content

Commit

Permalink
Make FlatList permissive of ArrayLike data (#36236)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #36236

D38198351 (d574ea3) addedd a guard to FlatList, to no-op if passed `data` that was not an array. This broke functionality where Realm had documented using `Realm.Results` with FlatList. `Real.Results` is an array-like JSI object, but not actually an array, and fails any `Array.isArray()` checks.

This change loosens the FlatList contract, to explicitly allow array-like non-array entities. The requirement align to Flow `ArrayLike`, which allows both arrays, and objects which provide a length and indexer. Flow `$ArrayLike` currently also requires an iterator, but this is seemingly a mistake in the type definition, and not enforced.

Though `Realm.Results` has all the methods of TS `ReadonlyArray`, RN has generally assumes its array inputs will pass `Array.isArray()`. This includes any array props still being checked [via prop-types](https://github.com/facebook/prop-types/blob/044efd7a108556c7660f6b62092756666e39d74b/factoryWithTypeCheckers.js#L548).

This change intentionally does not yet change the parameter type of `getItemLayout()`, which is already too loose (allowing mutable arrays). Changing this is a breaking change, that would be disruptive to backport, so we separate it into a different commit that will be landed as part of 0.72 (see next diff in the stack).

Changelog:
[General][Changed] - Make FlatList permissive of ArrayLike data

Reviewed By: yungsters

Differential Revision: D43465654

fbshipit-source-id: 3ed8c76c15da680560d7639b7cc43272f3e46ac3
  • Loading branch information
NickGerleman authored and kelset committed Mar 6, 2023
1 parent 267b31a commit 6c5088f
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 13 deletions.
12 changes: 6 additions & 6 deletions Libraries/Lists/FlatList.d.ts
Expand Up @@ -14,9 +14,9 @@ import type {
VirtualizedListProps,
} from './VirtualizedList';
import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView';
import {StyleProp} from '../StyleSheet/StyleSheet';
import {ViewStyle} from '../StyleSheet/StyleSheetTypes';
import {View} from '../Components/View/View';
import type {StyleProp} from '../StyleSheet/StyleSheet';
import type {ViewStyle} from '../StyleSheet/StyleSheetTypes';
import type {View} from '../Components/View/View';

export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
/**
Expand All @@ -40,10 +40,10 @@ export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
| undefined;

/**
* For simplicity, data is just a plain array. If you want to use something else,
* like an immutable list, use the underlying VirtualizedList directly.
* An array (or array-like list) of items to render. Other data types can be
* used by targetting VirtualizedList directly.
*/
data: ReadonlyArray<ItemT> | null | undefined;
data: ArrayLike<ItemT> | null | undefined;

/**
* A marker property for telling the list to re-render (since it implements PureComponent).
Expand Down
27 changes: 20 additions & 7 deletions Libraries/Lists/FlatList.js
Expand Up @@ -30,10 +30,10 @@ const React = require('react');

type RequiredProps<ItemT> = {|
/**
* For simplicity, data is just a plain array. If you want to use something else, like an
* immutable list, use the underlying `VirtualizedList` directly.
* An array (or array-like list) of items to render. Other data types can be
* used by targetting VirtualizedList directly.
*/
data: ?$ReadOnlyArray<ItemT>,
data: ?$ArrayLike<ItemT>,
|};
type OptionalProps<ItemT> = {|
/**
Expand Down Expand Up @@ -163,6 +163,11 @@ function numColumnsOrDefault(numColumns: ?number) {
return numColumns ?? 1;
}

function isArrayLike(data: mixed): boolean {
// $FlowExpectedError[incompatible-use]
return typeof Object(data).length === 'number';
}

type FlatListProps<ItemT> = {|
...RequiredProps<ItemT>,
...OptionalProps<ItemT>,
Expand Down Expand Up @@ -497,8 +502,10 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
);
}

// $FlowFixMe[missing-local-annot]
_getItem = (data: Array<ItemT>, index: number) => {
_getItem = (
data: $ArrayLike<ItemT>,
index: number,
): ?(ItemT | $ReadOnlyArray<ItemT>) => {
const numColumns = numColumnsOrDefault(this.props.numColumns);
if (numColumns > 1) {
const ret = [];
Expand All @@ -515,8 +522,14 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
}
};

_getItemCount = (data: ?Array<ItemT>): number => {
if (Array.isArray(data)) {
_getItemCount = (data: ?$ArrayLike<ItemT>): number => {
// Legacy behavior of FlatList was to forward "undefined" length if invalid
// data like a non-arraylike object is passed. VirtualizedList would then
// coerce this, and the math would work out to no-op. For compatibility, if
// invalid data is passed, we tell VirtualizedList there are zero items
// available to prevent it from trying to read from the invalid data
// (without propagating invalidly typed data).
if (data != null && isArrayLike(data)) {
const numColumns = numColumnsOrDefault(this.props.numColumns);
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
} else {
Expand Down
25 changes: 25 additions & 0 deletions Libraries/Lists/__tests__/FlatList-test.js
Expand Up @@ -182,4 +182,29 @@ describe('FlatList', () => {

expect(renderItemInThreeColumns).toHaveBeenCalledTimes(7);
});
it('renders array-like data', () => {
const arrayLike = {
length: 3,
0: {key: 'i1'},
1: {key: 'i2'},
2: {key: 'i3'},
};

const component = ReactTestRenderer.create(
<FlatList
data={arrayLike}
renderItem={({item}) => <item value={item.key} />}
/>,
);
expect(component).toMatchSnapshot();
});
it('ignores invalid data', () => {
const component = ReactTestRenderer.create(
<FlatList
data={123456}
renderItem={({item}) => <item value={item.key} />}
/>,
);
expect(component).toMatchSnapshot();
});
});
157 changes: 157 additions & 0 deletions Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap
@@ -1,5 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FlatList ignores invalid data 1`] = `
<RCTScrollView
alwaysBounceVertical={true}
data={123456}
getItem={[Function]}
getItemCount={[Function]}
keyExtractor={[Function]}
onContentSizeChange={null}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onResponderGrant={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
pagingEnabled={false}
removeClippedSubviews={false}
renderItem={[Function]}
scrollEventThrottle={50}
scrollViewRef={[Function]}
sendMomentumEvents={true}
snapToEnd={true}
snapToStart={true}
stickyHeaderIndices={Array []}
style={
Object {
"flexDirection": "column",
"flexGrow": 1,
"flexShrink": 1,
"overflow": "scroll",
}
}
viewabilityConfigCallbackPairs={Array []}
>
<RCTScrollContentView
collapsable={false}
onLayout={[Function]}
removeClippedSubviews={false}
style={
Array [
false,
undefined,
]
}
/>
</RCTScrollView>
`;

exports[`FlatList renders all the bells and whistles 1`] = `
<RCTScrollView
ItemSeparatorComponent={[Function]}
Expand Down Expand Up @@ -122,6 +180,105 @@ exports[`FlatList renders all the bells and whistles 1`] = `
</RCTScrollView>
`;

exports[`FlatList renders array-like data 1`] = `
<RCTScrollView
alwaysBounceVertical={true}
data={
Object {
"0": Object {
"key": "i1",
},
"1": Object {
"key": "i2",
},
"2": Object {
"key": "i3",
},
"length": 3,
}
}
getItem={[Function]}
getItemCount={[Function]}
keyExtractor={[Function]}
onContentSizeChange={null}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onResponderGrant={[Function]}
onResponderReject={[Function]}
onResponderRelease={[Function]}
onResponderTerminationRequest={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
onScrollShouldSetResponder={[Function]}
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
pagingEnabled={false}
removeClippedSubviews={false}
renderItem={[Function]}
scrollEventThrottle={50}
scrollViewRef={[Function]}
sendMomentumEvents={true}
snapToEnd={true}
snapToStart={true}
stickyHeaderIndices={Array []}
style={
Object {
"flexDirection": "column",
"flexGrow": 1,
"flexShrink": 1,
"overflow": "scroll",
}
}
viewabilityConfigCallbackPairs={Array []}
>
<RCTScrollContentView
collapsable={false}
onLayout={[Function]}
removeClippedSubviews={false}
style={
Array [
false,
undefined,
]
}
>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<item
value="i1"
/>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<item
value="i2"
/>
</View>
<View
onFocusCapture={[Function]}
onLayout={[Function]}
style={null}
>
<item
value="i3"
/>
</View>
</RCTScrollContentView>
</RCTScrollView>
`;

exports[`FlatList renders empty list 1`] = `
<RCTScrollView
data={Array []}
Expand Down

0 comments on commit 6c5088f

Please sign in to comment.