Skip to content

Commit

Permalink
Make FlatList permissive of ArrayLike data
Browse files Browse the repository at this point in the history
Summary:
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, indexer, and iterator.

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

Differential Revision: D43465654

fbshipit-source-id: b1f0c14305df5bf67f2d7112fcc5a95bb65d0351
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Feb 22, 2023
1 parent 407fb5c commit 648aa3a
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 13 deletions.
13 changes: 7 additions & 6 deletions Libraries/Lists/FlatList.d.ts
Expand Up @@ -14,9 +14,10 @@ import type {
VirtualizedListProps,
} from '@react-native/virtualized-lists';
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';
import type {$ArrayLike} from '../../types/public/FlowLib';

export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
/**
Expand All @@ -40,10 +41,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
21 changes: 14 additions & 7 deletions Libraries/Lists/FlatList.js
Expand Up @@ -33,10 +33,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 @@ -500,8 +500,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 @@ -518,8 +520,13 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
}
};

_getItemCount = (data: ?Array<ItemT>): number => {
if (Array.isArray(data)) {
_getItemCount = (data: ?$ArrayLike<ItemT>): number => {
if (
data &&
(typeof data === 'object' || typeof data === 'function') &&
typeof data.length === 'number' &&
Symbol.iterator in data
) {
const numColumns = numColumnsOrDefault(this.props.numColumns);
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
} else {
Expand Down
30 changes: 30 additions & 0 deletions Libraries/Lists/__tests__/FlatList-test.js
Expand Up @@ -182,4 +182,34 @@ describe('FlatList', () => {

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

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();
});
});
158 changes: 158 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,106 @@ 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,
Symbol(Symbol.iterator): [Function],
}
}
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
1 change: 1 addition & 0 deletions types/index.d.ts
Expand Up @@ -149,6 +149,7 @@ export * from '../Libraries/YellowBox/YellowBoxDeprecated';
export * from '../Libraries/vendor/core/ErrorUtils';

export * from './public/DeprecatedPropertiesAlias';
export * from './public/FlowLib';
export * from './public/Insets';
export * from './public/ReactNativeRenderer';
export * from './public/ReactNativeTypes';
Expand Down
16 changes: 16 additions & 0 deletions types/public/FlowLib.d.ts
@@ -0,0 +1,16 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

/**
* Read-only view over an array, or array-like object.
*
* See $ArrayLike
* https://github.com/facebook/flow/blob/d28e646ebb2c72d3ad7751fc0edcb6baba8d7fee/lib/core.js#L1085
*/
export type $ArrayLike<ItemT> = ArrayLike<ItemT> & Iterable<ItemT>;

0 comments on commit 648aa3a

Please sign in to comment.