mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Make FlatList permissive of ArrayLike data (#36236)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36236 D38198351 (https://github.com/facebook/react-native/commit/d574ea3526e713eae2c6e20c7a68fa66ff4ad7d2) 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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
33d4e2d617
commit
c03de97fb4
Vendored
+6
-6
@@ -14,9 +14,9 @@ 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';
|
||||
|
||||
export interface FlatListProps<ItemT> extends VirtualizedListProps<ItemT> {
|
||||
/**
|
||||
@@ -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).
|
||||
|
||||
@@ -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> = {|
|
||||
/**
|
||||
@@ -166,6 +166,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>,
|
||||
@@ -500,8 +505,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 = [];
|
||||
@@ -518,8 +525,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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]}
|
||||
@@ -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 []}
|
||||
|
||||
Reference in New Issue
Block a user