Fix bug - FlatList component did not render more items when content was filtered
Summary: **Bug Description** The FlatList component receives content items via the data prop, and renders an initial number of items on the app's view. When a user scrolls to the end of the list, the component will append and render more available items at the end of the list. There was a bug where when the content was filtered, there were more available items but the component did not append/render them. This is due to the current appending/rendering logic in VirtualizedList, which does not account for data changes as a condition for updating/rendering. VirtualizedList is a dependency of FlatList, so this issue affects FlatList as well. **Approach to Fixing Bug** (i) Reproduce bug on iOS view of FlatList. (ii) For VirtualizedList component: # Isolate onEndReached function that appends more data to component UI. # Isolate _onContentSizeChange function that is called when list content changes. # Write snapshot tests using jest, based off existing test for FlatList. # Refactor logic to append more data to list into _maybeCallOnEndReached function. # Call _maybeCallOnEndReached in both _onContentSizeChange and _onScroll. (iii) Run snapshot tests and observe jest output. (iv) Bring up iOS view of FlatList and check that component now renders more items when content is filtered. Many thanks to sahrens for guidance in developing this code! Reviewed By: sahrens Differential Revision: D4877388 fbshipit-source-id: c10c9eef1912f491450a62b81a9bc41f7f784203
This commit is contained in:
parent
ff056d22d9
commit
3e7aa5f14e
|
@ -623,12 +623,29 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
return !this.props.horizontal ? metrics.y : metrics.x;
|
||||
}
|
||||
|
||||
_maybeCallOnEndReached() {
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold} = this.props;
|
||||
const {contentLength, visibleLength, offset} = this._scrollMetrics;
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
if (onEndReached &&
|
||||
this.state.last === getItemCount(data) - 1 &&
|
||||
distanceFromEnd < onEndReachedThreshold * visibleLength &&
|
||||
(this._hasDataChangedSinceEndReached ||
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength)) {
|
||||
// Only call onEndReached once for a given dataset + content length.
|
||||
this._hasDataChangedSinceEndReached = false;
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
}
|
||||
}
|
||||
|
||||
_onContentSizeChange = (width: number, height: number) => {
|
||||
if (this.props.onContentSizeChange) {
|
||||
this.props.onContentSizeChange(width, height);
|
||||
}
|
||||
this._scrollMetrics.contentLength = this._selectLength({height, width});
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
this._maybeCallOnEndReached();
|
||||
};
|
||||
|
||||
_sampleFillRate(sampleType: string) {
|
||||
|
@ -662,7 +679,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
const dOffset = offset - this._scrollMetrics.offset;
|
||||
const velocity = dOffset / dt;
|
||||
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props;
|
||||
const {data, getItemCount, windowSize} = this.props;
|
||||
|
||||
this._sampleFillRate('onScroll');
|
||||
|
||||
|
@ -670,19 +687,10 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
const itemCount = getItemCount(data);
|
||||
if (onEndReached &&
|
||||
this.state.last === itemCount - 1 &&
|
||||
distanceFromEnd < onEndReachedThreshold * visibleLength &&
|
||||
(this._hasDataChangedSinceEndReached ||
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength)) {
|
||||
// Only call onEndReached once for a given dataset + content length.
|
||||
this._hasDataChangedSinceEndReached = false;
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
}
|
||||
this._maybeCallOnEndReached();
|
||||
|
||||
const {first, last} = this.state;
|
||||
const itemCount = getItemCount(data);
|
||||
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
|
||||
const distanceToContentEdge = Math.min(
|
||||
Math.abs(this._getFrameMetricsApprox(first).offset - offset),
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
jest.disableAutomock();
|
||||
|
||||
const React = require('React');
|
||||
const ReactTestRenderer = require('react-test-renderer');
|
||||
|
||||
const VirtualizedList = require('VirtualizedList');
|
||||
|
||||
describe('VirtualizedList', () => {
|
||||
|
||||
it('renders simple list', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={[{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders empty list', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={[]}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders null list', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={undefined}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders all the bells and whistles', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
ItemSeparatorComponent={() => <separator />}
|
||||
ListFooterComponent={() => <footer />}
|
||||
ListHeaderComponent={() => <header />}
|
||||
data={new Array(5).fill().map((_, ii) => ({id: String(ii)}))}
|
||||
keyExtractor={(item, index) => item.id}
|
||||
getItemLayout={({index}) => ({length: 50, offset: index * 50})}
|
||||
numColumns={2}
|
||||
refreshing={false}
|
||||
onRefresh={jest.fn()}
|
||||
renderItem={({item}) => <item value={item.id} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('test getItem functionality where data is not an Array', () => {
|
||||
const component = ReactTestRenderer.create(
|
||||
<VirtualizedList
|
||||
data={new Map([['id_0', {key: 'item_0'}]])}
|
||||
getItem={(data, index) => data.get('id_' + index)}
|
||||
getItemCount={(data: Map) => data.size}
|
||||
renderItem={({item}) => <item value={item.key} />}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,261 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VirtualizedList renders all the bells and whistles 1`] = `
|
||||
<RCTScrollView
|
||||
ItemSeparatorComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
ListHeaderComponent={[Function]}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"id": "0",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
},
|
||||
Object {
|
||||
"id": "3",
|
||||
},
|
||||
Object {
|
||||
"id": "4",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
getItemLayout={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
numColumns={2}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onRefresh={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
refreshControl={
|
||||
<RefreshControlMock
|
||||
onRefresh={[Function]}
|
||||
refreshing={false}
|
||||
/>
|
||||
}
|
||||
refreshing={false}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<RCTRefreshControl />
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<header />
|
||||
</View>
|
||||
<View
|
||||
onLayout={undefined}
|
||||
>
|
||||
<item
|
||||
value="0"
|
||||
/>
|
||||
<separator />
|
||||
</View>
|
||||
<View
|
||||
onLayout={undefined}
|
||||
>
|
||||
<item
|
||||
value="1"
|
||||
/>
|
||||
<separator />
|
||||
</View>
|
||||
<View
|
||||
onLayout={undefined}
|
||||
>
|
||||
<item
|
||||
value="2"
|
||||
/>
|
||||
<separator />
|
||||
</View>
|
||||
<View
|
||||
onLayout={undefined}
|
||||
>
|
||||
<item
|
||||
value="3"
|
||||
/>
|
||||
<separator />
|
||||
</View>
|
||||
<View
|
||||
onLayout={undefined}
|
||||
>
|
||||
<item
|
||||
value="4"
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<footer />
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList renders empty list 1`] = `
|
||||
<RCTScrollView
|
||||
data={Array []}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View />
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList renders null list 1`] = `
|
||||
<RCTScrollView
|
||||
data={undefined}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View />
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList renders simple list 1`] = `
|
||||
<RCTScrollView
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"key": "i1",
|
||||
},
|
||||
Object {
|
||||
"key": "i2",
|
||||
},
|
||||
Object {
|
||||
"key": "i3",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
value="i1"
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
value="i2"
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
value="i3"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`VirtualizedList test getItem functionality where data is not an Array 1`] = `
|
||||
<RCTScrollView
|
||||
data={
|
||||
Map {
|
||||
"id_0" => Object {
|
||||
"key": "item_0",
|
||||
},
|
||||
}
|
||||
}
|
||||
disableVirtualization={false}
|
||||
getItem={[Function]}
|
||||
getItemCount={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
maxToRenderPerBatch={10}
|
||||
onContentSizeChange={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
renderItem={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
stickyHeaderIndices={Array []}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
>
|
||||
<item
|
||||
value="item_0"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
Loading…
Reference in New Issue