Skip to content

Commit c03de97

Browse files
NickGerlemanfacebook-github-bot
authored andcommitted
Make FlatList permissive of ArrayLike data (#36236)
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
1 parent 33d4e2d commit c03de97

File tree

4 files changed

+208
-13
lines changed

4 files changed

+208
-13
lines changed

Libraries/Lists/FlatList.d.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ 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';
2020

2121
export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
2222
/**
@@ -40,10 +40,10 @@ export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
4040
| undefined;
4141

4242
/**
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.
43+
* An array (or array-like list) of items to render. Other data types can be
44+
* used by targetting VirtualizedList directly.
4545
*/
46-
data: ReadonlyArray<ItemT> | null | undefined;
46+
data: ArrayLike<ItemT> | null | undefined;
4747

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

Libraries/Lists/FlatList.js

Lines changed: 20 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
/**
@@ -166,6 +166,11 @@ function numColumnsOrDefault(numColumns: ?number) {
166166
return numColumns ?? 1;
167167
}
168168

169+
function isArrayLike(data: mixed): boolean {
170+
// $FlowExpectedError[incompatible-use]
171+
return typeof Object(data).length === 'number';
172+
}
173+
169174
type FlatListProps<ItemT> = {|
170175
...RequiredProps<ItemT>,
171176
...OptionalProps<ItemT>,
@@ -500,8 +505,10 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
500505
);
501506
}
502507

503-
// $FlowFixMe[missing-local-annot]
504-
_getItem = (data: Array<ItemT>, index: number) => {
508+
_getItem = (
509+
data: $ArrayLike<ItemT>,
510+
index: number,
511+
): ?(ItemT | $ReadOnlyArray<ItemT>) => {
505512
const numColumns = numColumnsOrDefault(this.props.numColumns);
506513
if (numColumns > 1) {
507514
const ret = [];
@@ -518,8 +525,14 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
518525
}
519526
};
520527

521-
_getItemCount = (data: ?Array<ItemT>): number => {
522-
if (Array.isArray(data)) {
528+
_getItemCount = (data: ?$ArrayLike<ItemT>): number => {
529+
// Legacy behavior of FlatList was to forward "undefined" length if invalid
530+
// data like a non-arraylike object is passed. VirtualizedList would then
531+
// coerce this, and the math would work out to no-op. For compatibility, if
532+
// invalid data is passed, we tell VirtualizedList there are zero items
533+
// available to prevent it from trying to read from the invalid data
534+
// (without propagating invalidly typed data).
535+
if (data != null && isArrayLike(data)) {
523536
const numColumns = numColumnsOrDefault(this.props.numColumns);
524537
return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length;
525538
} else {

Libraries/Lists/__tests__/FlatList-test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,29 @@ 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+
};
192+
193+
const component = ReactTestRenderer.create(
194+
<FlatList
195+
data={arrayLike}
196+
renderItem={({item}) => <item value={item.key} />}
197+
/>,
198+
);
199+
expect(component).toMatchSnapshot();
200+
});
201+
it('ignores invalid data', () => {
202+
const component = ReactTestRenderer.create(
203+
<FlatList
204+
data={123456}
205+
renderItem={({item}) => <item value={item.key} />}
206+
/>,
207+
);
208+
expect(component).toMatchSnapshot();
209+
});
185210
});

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

Lines changed: 157 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,105 @@ 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+
}
199+
}
200+
getItem={[Function]}
201+
getItemCount={[Function]}
202+
keyExtractor={[Function]}
203+
onContentSizeChange={null}
204+
onLayout={[Function]}
205+
onMomentumScrollBegin={[Function]}
206+
onMomentumScrollEnd={[Function]}
207+
onResponderGrant={[Function]}
208+
onResponderReject={[Function]}
209+
onResponderRelease={[Function]}
210+
onResponderTerminationRequest={[Function]}
211+
onScroll={[Function]}
212+
onScrollBeginDrag={[Function]}
213+
onScrollEndDrag={[Function]}
214+
onScrollShouldSetResponder={[Function]}
215+
onStartShouldSetResponder={[Function]}
216+
onStartShouldSetResponderCapture={[Function]}
217+
onTouchCancel={[Function]}
218+
onTouchEnd={[Function]}
219+
onTouchMove={[Function]}
220+
onTouchStart={[Function]}
221+
pagingEnabled={false}
222+
removeClippedSubviews={false}
223+
renderItem={[Function]}
224+
scrollEventThrottle={50}
225+
scrollViewRef={[Function]}
226+
sendMomentumEvents={true}
227+
snapToEnd={true}
228+
snapToStart={true}
229+
stickyHeaderIndices={Array []}
230+
style={
231+
Object {
232+
"flexDirection": "column",
233+
"flexGrow": 1,
234+
"flexShrink": 1,
235+
"overflow": "scroll",
236+
}
237+
}
238+
viewabilityConfigCallbackPairs={Array []}
239+
>
240+
<RCTScrollContentView
241+
collapsable={false}
242+
onLayout={[Function]}
243+
removeClippedSubviews={false}
244+
style={
245+
Array [
246+
false,
247+
undefined,
248+
]
249+
}
250+
>
251+
<View
252+
onFocusCapture={[Function]}
253+
onLayout={[Function]}
254+
style={null}
255+
>
256+
<item
257+
value="i1"
258+
/>
259+
</View>
260+
<View
261+
onFocusCapture={[Function]}
262+
onLayout={[Function]}
263+
style={null}
264+
>
265+
<item
266+
value="i2"
267+
/>
268+
</View>
269+
<View
270+
onFocusCapture={[Function]}
271+
onLayout={[Function]}
272+
style={null}
273+
>
274+
<item
275+
value="i3"
276+
/>
277+
</View>
278+
</RCTScrollContentView>
279+
</RCTScrollView>
280+
`;
281+
125282
exports[`FlatList renders empty list 1`] = `
126283
<RCTScrollView
127284
data={Array []}

0 commit comments

Comments
 (0)