Skip to content

Commit 648aa3a

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Make FlatList permissive of ArrayLike data
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
1 parent 407fb5c commit 648aa3a

File tree

6 files changed

+226
-13
lines changed

6 files changed

+226
-13
lines changed

Libraries/Lists/FlatList.d.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import type {
1414
VirtualizedListProps,
1515
} from '@react-native/virtualized-lists';
1616
import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView';
17-
import {StyleProp} from '../StyleSheet/StyleSheet';
18-
import {ViewStyle} from '../StyleSheet/StyleSheetTypes';
19-
import {View} from '../Components/View/View';
17+
import type {StyleProp} from '../StyleSheet/StyleSheet';
18+
import type {ViewStyle} from '../StyleSheet/StyleSheetTypes';
19+
import type {View} from '../Components/View/View';
20+
import type {$ArrayLike} from '../../types/public/FlowLib';
2021

2122
export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
2223
/**
@@ -40,10 +41,10 @@ export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
4041
| undefined;
4142

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

4849
/**
4950
* A marker property for telling the list to re-render (since it implements PureComponent).

Libraries/Lists/FlatList.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ const React = require('react');
3333

3434
type RequiredProps<ItemT> = {|
3535
/**
36-
* For simplicity, data is just a plain array. If you want to use something else, like an
37-
* immutable list, use the underlying `VirtualizedList` directly.
36+
* An array (or array-like list) of items to render. Other data types can be
37+
* used by targetting VirtualizedList directly.
3838
*/
39-
data: ?$ReadOnlyArray<ItemT>,
39+
data: ?$ArrayLike<ItemT>,
4040
|};
4141
type OptionalProps<ItemT> = {|
4242
/**
@@ -500,8 +500,10 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
500500
);
501501
}
502502

503-
// $FlowFixMe[missing-local-annot]
504-
_getItem = (data: Array<ItemT>, index: number) => {
503+
_getItem = (
504+
data: $ArrayLike<ItemT>,
505+
index: number,
506+
): ?(ItemT | $ReadOnlyArray<ItemT>) => {
505507
const numColumns = numColumnsOrDefault(this.props.numColumns);
506508
if (numColumns > 1) {
507509
const ret = [];
@@ -518,8 +520,13 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
518520
}
519521
};
520522

521-
_getItemCount = (data: ?Array<ItemT>): number => {
522-
if (Array.isArray(data)) {
523+
_getItemCount = (data: ?$ArrayLike<ItemT>): number => {
524+
if (
525+
data &&
526+
(typeof data === 'object' || typeof data === 'function') &&
527+
typeof data.length === 'number' &&
528+
Symbol.iterator in data
529+
) {
523530
const numColumns = numColumnsOrDefault(this.props.numColumns);
524531
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
525532
} else {

Libraries/Lists/__tests__/FlatList-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,34 @@ describe('FlatList', () => {
182182

183183
expect(renderItemInThreeColumns).toHaveBeenCalledTimes(7);
184184
});
185+
it('renders array-like data', () => {
186+
const arrayLike = {
187+
length: 3,
188+
0: {key: 'i1'},
189+
1: {key: 'i2'},
190+
2: {key: 'i3'},
191+
*[Symbol.iterator]() {
192+
yield arrayLike[0];
193+
yield arrayLike[1];
194+
yield arrayLike[2];
195+
},
196+
};
197+
198+
const component = ReactTestRenderer.create(
199+
<FlatList
200+
data={arrayLike}
201+
renderItem={({item}) => <item value={item.key} />}
202+
/>,
203+
);
204+
expect(component).toMatchSnapshot();
205+
});
206+
it('ignores invalid data', () => {
207+
const component = ReactTestRenderer.create(
208+
<FlatList
209+
data={123456}
210+
renderItem={({item}) => <item value={item.key} />}
211+
/>,
212+
);
213+
expect(component).toMatchSnapshot();
214+
});
185215
});

Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,63 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`FlatList ignores invalid data 1`] = `
4+
<RCTScrollView
5+
alwaysBounceVertical={true}
6+
data={123456}
7+
getItem={[Function]}
8+
getItemCount={[Function]}
9+
keyExtractor={[Function]}
10+
onContentSizeChange={null}
11+
onLayout={[Function]}
12+
onMomentumScrollBegin={[Function]}
13+
onMomentumScrollEnd={[Function]}
14+
onResponderGrant={[Function]}
15+
onResponderReject={[Function]}
16+
onResponderRelease={[Function]}
17+
onResponderTerminationRequest={[Function]}
18+
onScroll={[Function]}
19+
onScrollBeginDrag={[Function]}
20+
onScrollEndDrag={[Function]}
21+
onScrollShouldSetResponder={[Function]}
22+
onStartShouldSetResponder={[Function]}
23+
onStartShouldSetResponderCapture={[Function]}
24+
onTouchCancel={[Function]}
25+
onTouchEnd={[Function]}
26+
onTouchMove={[Function]}
27+
onTouchStart={[Function]}
28+
pagingEnabled={false}
29+
removeClippedSubviews={false}
30+
renderItem={[Function]}
31+
scrollEventThrottle={50}
32+
scrollViewRef={[Function]}
33+
sendMomentumEvents={true}
34+
snapToEnd={true}
35+
snapToStart={true}
36+
stickyHeaderIndices={Array []}
37+
style={
38+
Object {
39+
"flexDirection": "column",
40+
"flexGrow": 1,
41+
"flexShrink": 1,
42+
"overflow": "scroll",
43+
}
44+
}
45+
viewabilityConfigCallbackPairs={Array []}
46+
>
47+
<RCTScrollContentView
48+
collapsable={false}
49+
onLayout={[Function]}
50+
removeClippedSubviews={false}
51+
style={
52+
Array [
53+
false,
54+
undefined,
55+
]
56+
}
57+
/>
58+
</RCTScrollView>
59+
`;
60+
361
exports[`FlatList renders all the bells and whistles 1`] = `
462
<RCTScrollView
563
ItemSeparatorComponent={[Function]}
@@ -122,6 +180,106 @@ exports[`FlatList renders all the bells and whistles 1`] = `
122180
</RCTScrollView>
123181
`;
124182

183+
exports[`FlatList renders array-like data 1`] = `
184+
<RCTScrollView
185+
alwaysBounceVertical={true}
186+
data={
187+
Object {
188+
"0": Object {
189+
"key": "i1",
190+
},
191+
"1": Object {
192+
"key": "i2",
193+
},
194+
"2": Object {
195+
"key": "i3",
196+
},
197+
"length": 3,
198+
Symbol(Symbol.iterator): [Function],
199+
}
200+
}
201+
getItem={[Function]}
202+
getItemCount={[Function]}
203+
keyExtractor={[Function]}
204+
onContentSizeChange={null}
205+
onLayout={[Function]}
206+
onMomentumScrollBegin={[Function]}
207+
onMomentumScrollEnd={[Function]}
208+
onResponderGrant={[Function]}
209+
onResponderReject={[Function]}
210+
onResponderRelease={[Function]}
211+
onResponderTerminationRequest={[Function]}
212+
onScroll={[Function]}
213+
onScrollBeginDrag={[Function]}
214+
onScrollEndDrag={[Function]}
215+
onScrollShouldSetResponder={[Function]}
216+
onStartShouldSetResponder={[Function]}
217+
onStartShouldSetResponderCapture={[Function]}
218+
onTouchCancel={[Function]}
219+
onTouchEnd={[Function]}
220+
onTouchMove={[Function]}
221+
onTouchStart={[Function]}
222+
pagingEnabled={false}
223+
removeClippedSubviews={false}
224+
renderItem={[Function]}
225+
scrollEventThrottle={50}
226+
scrollViewRef={[Function]}
227+
sendMomentumEvents={true}
228+
snapToEnd={true}
229+
snapToStart={true}
230+
stickyHeaderIndices={Array []}
231+
style={
232+
Object {
233+
"flexDirection": "column",
234+
"flexGrow": 1,
235+
"flexShrink": 1,
236+
"overflow": "scroll",
237+
}
238+
}
239+
viewabilityConfigCallbackPairs={Array []}
240+
>
241+
<RCTScrollContentView
242+
collapsable={false}
243+
onLayout={[Function]}
244+
removeClippedSubviews={false}
245+
style={
246+
Array [
247+
false,
248+
undefined,
249+
]
250+
}
251+
>
252+
<View
253+
onFocusCapture={[Function]}
254+
onLayout={[Function]}
255+
style={null}
256+
>
257+
<item
258+
value="i1"
259+
/>
260+
</View>
261+
<View
262+
onFocusCapture={[Function]}
263+
onLayout={[Function]}
264+
style={null}
265+
>
266+
<item
267+
value="i2"
268+
/>
269+
</View>
270+
<View
271+
onFocusCapture={[Function]}
272+
onLayout={[Function]}
273+
style={null}
274+
>
275+
<item
276+
value="i3"
277+
/>
278+
</View>
279+
</RCTScrollContentView>
280+
</RCTScrollView>
281+
`;
282+
125283
exports[`FlatList renders empty list 1`] = `
126284
<RCTScrollView
127285
data={Array []}

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export * from '../Libraries/YellowBox/YellowBoxDeprecated';
149149
export * from '../Libraries/vendor/core/ErrorUtils';
150150

151151
export * from './public/DeprecatedPropertiesAlias';
152+
export * from './public/FlowLib';
152153
export * from './public/Insets';
153154
export * from './public/ReactNativeRenderer';
154155
export * from './public/ReactNativeTypes';

types/public/FlowLib.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
/**
11+
* Read-only view over an array, or array-like object.
12+
*
13+
* See $ArrayLike
14+
* https://github.com/facebook/flow/blob/d28e646ebb2c72d3ad7751fc0edcb6baba8d7fee/lib/core.js#L1085
15+
*/
16+
export type $ArrayLike<ItemT> = ArrayLike<ItemT> & Iterable<ItemT>;

0 commit comments

Comments
 (0)