Skip to content

Commit 63f7efc

Browse files
sahrensfacebook-github-bot
authored andcommitted
Add basic nested VirtualizedList support
Summary: This uses `context` to render inner lists of the same orientation to a plain `View` without virtualization instead of rendering nested `ScrollView`s trying to scroll in the same direction, which can cause problems. Reviewed By: bvaughn Differential Revision: D5174942 fbshipit-source-id: 989150294098de837b0ffb401c7f5679a3928a03
1 parent 2c32acb commit 63f7efc

File tree

5 files changed

+243
-58
lines changed

5 files changed

+243
-58
lines changed

Libraries/Lists/VirtualizedList.js

Lines changed: 82 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
const Batchinator = require('Batchinator');
1515
const FillRateHelper = require('FillRateHelper');
16+
const PropTypes = require('prop-types');
1617
const React = require('React');
1718
const ReactNative = require('ReactNative');
1819
const RefreshControl = require('RefreshControl');
@@ -139,7 +140,7 @@ type OptionalProps = {
139140
/**
140141
* Render a custom scroll component, e.g. with a differently styled `RefreshControl`.
141142
*/
142-
renderScrollComponent: (props: Object) => React.Element<any>,
143+
renderScrollComponent?: (props: Object) => React.Element<any>,
143144
/**
144145
* Amount of time between low-pri item render batches, e.g. for rendering items quite a ways off
145146
* screen. Similar fill rate/responsiveness tradeoff as `maxToRenderPerBatch`.
@@ -301,35 +302,32 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
301302
},
302303
maxToRenderPerBatch: 10,
303304
onEndReachedThreshold: 2, // multiples of length
304-
renderScrollComponent: (props: Props) => {
305-
if (props.onRefresh) {
306-
invariant(
307-
typeof props.refreshing === 'boolean',
308-
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
309-
JSON.stringify(props.refreshing) + '`',
310-
);
311-
312-
return (
313-
<ScrollView
314-
{...props}
315-
refreshControl={
316-
<RefreshControl
317-
refreshing={props.refreshing}
318-
onRefresh={props.onRefresh}
319-
progressViewOffset={props.progressViewOffset}
320-
/>
321-
}
322-
/>
323-
);
324-
} else {
325-
return <ScrollView {...props} />;
326-
}
327-
},
328305
scrollEventThrottle: 50,
329306
updateCellsBatchingPeriod: 50,
330307
windowSize: 21, // multiples of length
331308
};
332309

310+
static contextTypes = {
311+
virtualizedList: PropTypes.shape({
312+
horizontal: PropTypes.bool,
313+
}),
314+
};
315+
316+
static childContextTypes = {
317+
virtualizedList: PropTypes.shape({
318+
horizontal: PropTypes.bool,
319+
}),
320+
};
321+
322+
getChildContext() {
323+
return {
324+
virtualizedList: {
325+
horizontal: this.props.horizontal,
326+
// TODO: support nested virtualization and onViewableItemsChanged
327+
},
328+
};
329+
}
330+
333331
state: State;
334332

335333
constructor(props: Props, context: Object) {
@@ -339,6 +337,11 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
339337
'Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent ' +
340338
'to support native onScroll events with useNativeDriver',
341339
);
340+
invariant(
341+
!(this._isNestedWithSameOrientation() && props.onViewableItemsChanged),
342+
'Nesting lists that scroll in the same direction does not support onViewableItemsChanged' +
343+
'on the inner list.'
344+
);
342345

343346
this._fillRateHelper = new FillRateHelper(this._getFrameMetrics);
344347
this._updateCellsToRenderBatcher = new Batchinator(
@@ -431,6 +434,15 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
431434
});
432435
};
433436

437+
_isVirtualizationDisabled(): bool {
438+
return this.props.disableVirtualization || this._isNestedWithSameOrientation();
439+
}
440+
441+
_isNestedWithSameOrientation(): bool {
442+
const nestedContext = this.context.virtualizedList;
443+
return !!(nestedContext && !!nestedContext.horizontal === !!this.props.horizontal);
444+
}
445+
434446
render() {
435447
if (__DEV__) {
436448
const flatStyles = flattenStyle(this.props.contentContainerStyle);
@@ -442,7 +454,8 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
442454
}
443455

444456
const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} = this.props;
445-
const {data, disableVirtualization, horizontal} = this.props;
457+
const {data, horizontal} = this.props;
458+
const isVirtualizationDisabled = this._isVirtualizationDisabled();
446459
const cells = [];
447460
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
448461
const stickyHeaderIndices = [];
@@ -466,7 +479,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
466479
const {first, last} = this.state;
467480
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, 0, lastInitialIndex);
468481
const firstAfterInitial = Math.max(lastInitialIndex + 1, first);
469-
if (!disableVirtualization && first > lastInitialIndex + 1) {
482+
if (!isVirtualizationDisabled && first > lastInitialIndex + 1) {
470483
let insertedStickySpacer = false;
471484
if (stickyIndicesFromProps.size > 0) {
472485
const stickyOffset = ListHeaderComponent ? 1 : 0;
@@ -507,7 +520,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
507520
);
508521
this._hasWarned.keys = true;
509522
}
510-
if (!disableVirtualization && last < itemCount - 1) {
523+
if (!isVirtualizationDisabled && last < itemCount - 1) {
511524
const lastFrame = this._getFrameMetricsApprox(last);
512525
// Without getItemLayout, we limit our tail spacer to the _highestMeasuredFrameIndex to
513526
// prevent the user for hyperscrolling into un-measured area because otherwise content will
@@ -543,18 +556,21 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
543556
</View>
544557
);
545558
}
559+
const scrollProps = {
560+
...this.props,
561+
onContentSizeChange: this._onContentSizeChange,
562+
onLayout: this._onLayout,
563+
onScroll: this._onScroll,
564+
onScrollBeginDrag: this._onScrollBeginDrag,
565+
onScrollEndDrag: this._onScrollEndDrag,
566+
onMomentumScrollEnd: this._onMomentumScrollEnd,
567+
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
568+
stickyHeaderIndices,
569+
};
546570
const ret = React.cloneElement(
547-
this.props.renderScrollComponent(this.props),
571+
(this.props.renderScrollComponent || this._defaultRenderScrollComponent)(scrollProps),
548572
{
549-
onContentSizeChange: this._onContentSizeChange,
550-
onLayout: this._onLayout,
551-
onScroll: this._onScroll,
552-
onScrollBeginDrag: this._onScrollBeginDrag,
553-
onScrollEndDrag: this._onScrollEndDrag,
554-
onMomentumScrollEnd: this._onMomentumScrollEnd,
555573
ref: this._captureScrollRef,
556-
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
557-
stickyHeaderIndices,
558574
},
559575
cells,
560576
);
@@ -601,6 +617,32 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
601617
);
602618
}
603619

620+
_defaultRenderScrollComponent = (props) => {
621+
if (this._isNestedWithSameOrientation()) {
622+
return <View {...props} />;
623+
} else if (props.onRefresh) {
624+
invariant(
625+
typeof props.refreshing === 'boolean',
626+
'`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `' +
627+
JSON.stringify(props.refreshing) + '`',
628+
);
629+
return (
630+
<ScrollView
631+
{...props}
632+
refreshControl={
633+
<RefreshControl
634+
refreshing={props.refreshing}
635+
onRefresh={props.onRefresh}
636+
progressViewOffset={props.progressViewOffset}
637+
/>
638+
}
639+
/>
640+
);
641+
} else {
642+
return <ScrollView {...props} />;
643+
}
644+
};
645+
604646
_onCellLayout(e, cellKey, index) {
605647
const layout = e.nativeEvent.layout;
606648
const next = {
@@ -816,14 +858,15 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
816858
};
817859

818860
_updateCellsToRender = () => {
819-
const {data, disableVirtualization, getItemCount, onEndReachedThreshold} = this.props;
861+
const {data, getItemCount, onEndReachedThreshold} = this.props;
862+
const isVirtualizationDisabled = this._isVirtualizationDisabled();
820863
this._updateViewableItems(data);
821864
if (!data) {
822865
return;
823866
}
824867
this.setState((state) => {
825868
let newState;
826-
if (!disableVirtualization) {
869+
if (!isVirtualizationDisabled) {
827870
newState = computeWindowedRenderLimits(
828871
this.props, state, this._getFrameMetricsApprox, this._scrollMetrics,
829872
);

Libraries/Lists/__tests__/VirtualizedList-test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,26 @@ describe('VirtualizedList', () => {
135135
expect(component).toMatchSnapshot();
136136
infos[1].separators.unhighlight();
137137
});
138+
139+
it('handles nested lists', () => {
140+
const component = ReactTestRenderer.create(
141+
<VirtualizedList
142+
data={[{key: 'outer0'}, {key: 'outer1'}]}
143+
renderItem={(outerInfo) => (
144+
<VirtualizedList
145+
data={[{key: outerInfo.item.key + ':inner0'}, {key: outerInfo.item.key + ':inner1'}]}
146+
horizontal={outerInfo.item.key === 'outer1'}
147+
renderItem={(innerInfo) => {
148+
return <item title={innerInfo.item.key} />;
149+
}}
150+
getItem={(data, index) => data[index]}
151+
getItemCount={(data) => data.length}
152+
/>
153+
)}
154+
getItem={(data, index) => data[index]}
155+
getItemCount={(data) => data.length}
156+
/>
157+
);
158+
expect(component).toMatchSnapshot();
159+
});
138160
});

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ exports[`FlatList renders all the bells and whistles 1`] = `
5252
}
5353
refreshing={false}
5454
renderItem={[Function]}
55-
renderScrollComponent={[Function]}
5655
scrollEventThrottle={50}
5756
stickyHeaderIndices={Array []}
5857
updateCellsBatchingPeriod={50}
@@ -156,7 +155,6 @@ exports[`FlatList renders empty list 1`] = `
156155
onScrollEndDrag={[Function]}
157156
onViewableItemsChanged={undefined}
158157
renderItem={[Function]}
159-
renderScrollComponent={[Function]}
160158
scrollEventThrottle={50}
161159
stickyHeaderIndices={Array []}
162160
updateCellsBatchingPeriod={50}
@@ -186,7 +184,6 @@ exports[`FlatList renders null list 1`] = `
186184
onScrollEndDrag={[Function]}
187185
onViewableItemsChanged={undefined}
188186
renderItem={[Function]}
189-
renderScrollComponent={[Function]}
190187
scrollEventThrottle={50}
191188
stickyHeaderIndices={Array []}
192189
updateCellsBatchingPeriod={50}
@@ -228,7 +225,6 @@ exports[`FlatList renders simple list 1`] = `
228225
onScrollEndDrag={[Function]}
229226
onViewableItemsChanged={undefined}
230227
renderItem={[Function]}
231-
renderScrollComponent={[Function]}
232228
scrollEventThrottle={50}
233229
stickyHeaderIndices={Array []}
234230
updateCellsBatchingPeriod={50}

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
3434
onScrollEndDrag={[Function]}
3535
onViewableItemsChanged={undefined}
3636
renderItem={[Function]}
37-
renderScrollComponent={[Function]}
3837
renderSectionHeader={[Function]}
3938
scrollEventThrottle={50}
4039
sections={
@@ -113,7 +112,6 @@ exports[`SectionList renders a footer when there is no data 1`] = `
113112
onScrollEndDrag={[Function]}
114113
onViewableItemsChanged={undefined}
115114
renderItem={[Function]}
116-
renderScrollComponent={[Function]}
117115
renderSectionFooter={[Function]}
118116
renderSectionHeader={[Function]}
119117
scrollEventThrottle={50}
@@ -180,7 +178,6 @@ exports[`SectionList renders a footer when there is no data and no header 1`] =
180178
onScrollEndDrag={[Function]}
181179
onViewableItemsChanged={undefined}
182180
renderItem={[Function]}
183-
renderScrollComponent={[Function]}
184181
renderSectionFooter={[Function]}
185182
scrollEventThrottle={50}
186183
sections={
@@ -287,7 +284,6 @@ exports[`SectionList renders all the bells and whistles 1`] = `
287284
}
288285
refreshing={false}
289286
renderItem={[Function]}
290-
renderScrollComponent={[Function]}
291287
renderSectionFooter={[Function]}
292288
renderSectionHeader={[Function]}
293289
scrollEventThrottle={50}
@@ -505,7 +501,6 @@ exports[`SectionList renders empty list 1`] = `
505501
onScrollEndDrag={[Function]}
506502
onViewableItemsChanged={undefined}
507503
renderItem={[Function]}
508-
renderScrollComponent={[Function]}
509504
scrollEventThrottle={50}
510505
sections={Array []}
511506
stickyHeaderIndices={Array []}

0 commit comments

Comments
 (0)