Skip to content

Commit

Permalink
Experimental VirtualList Optimization (facebook#27115)
Browse files Browse the repository at this point in the history
Summary:
This is VirtualList Optimization done by christianbach in facebook#21163 enabled behind a prop: `experimentalVirtualizedListOpt` with backward compatibility.

Fixes facebook#20174 based of jasekiw pull request facebook#20208

## Changelog

// TODO:

[CATEGORY] [TYPE] - Message

Pull Request resolved: facebook#27115

Test Plan:
// TODO:
Add tests related to backward compatibility. (Will need help)

Differential Revision: D30095387

Pulled By: lunaleaps

fbshipit-source-id: 1c41e9e52beeb79b56b19dfb12d896a2c4c49529
  • Loading branch information
azizhk authored and facebook-github-bot committed Aug 4, 2021
1 parent 363a8fb commit bd4bdfc
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 27 deletions.
7 changes: 6 additions & 1 deletion Libraries/Components/ScrollView/ScrollViewStickyHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import StyleSheet from '../../StyleSheet/StyleSheet';
import Animated from '../../Animated/Animated';
import * as React from 'react';
import {useEffect, useMemo, useRef, useCallback} from 'react';
import VirtualizedListInjection from '../../Lists/VirtualizedListInjection';

const AnimatedView = Animated.View;

Expand Down Expand Up @@ -264,7 +265,11 @@ const ScrollViewStickyHeaderWithForwardedRef: React.AbstractComponent<
props.onLayout(event);
const child = React.Children.only(props.children);
if (child.props.onLayout) {
child.props.onLayout(event);
if (VirtualizedListInjection.useVLOptimization) {
child.props.onLayout(event, child.props.cellKey, child.props.index);
} else {
child.props.onLayout(event);
}
}
};

Expand Down
97 changes: 71 additions & 26 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ScrollView = require('../Components/ScrollView/ScrollView');
const StyleSheet = require('../StyleSheet/StyleSheet');
const View = require('../Components/View/View');
const ViewabilityHelper = require('./ViewabilityHelper');
import VirtualizedListInjection from './VirtualizedListInjection';

const flattenStyle = require('../StyleSheet/flattenStyle');
const infoLog = require('../Utilities/infoLog');
Expand Down Expand Up @@ -221,6 +222,7 @@ type OptionalProps = {|
* within half the visible length of the list.
*/
onEndReachedThreshold?: ?number,
onLayout?: ?(e: LayoutEvent) => void,
/**
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
* sure to also set the `refreshing` prop correctly.
Expand Down Expand Up @@ -810,27 +812,45 @@ class VirtualizedList extends React.PureComponent<Props, State> {
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
stickyHeaderIndices.push(cells.length);
}
cells.push(
<CellRenderer
CellRendererComponent={CellRendererComponent}
ItemSeparatorComponent={ii < end ? ItemSeparatorComponent : undefined}
cellKey={key}
fillRateHelper={this._fillRateHelper}
horizontal={horizontal}
index={ii}
inversionStyle={inversionStyle}
item={item}
key={key}
prevCellKey={prevCellKey}
onUpdateSeparators={this._onUpdateSeparators}
onLayout={e => this._onCellLayout(e, key, ii)}
onUnmount={this._onCellUnmount}
parentProps={this.props}
ref={ref => {
this._cellRefs[key] = ref;
}}
/>,
);
const cellRendererBaseProps: CellRendererBaseProps = {
CellRendererComponent: CellRendererComponent,
ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined,
cellKey: key,
fillRateHelper: this._fillRateHelper,
horizontal: horizontal,
index: ii,
inversionStyle: inversionStyle,
item: item,
key: key,
prevCellKey: prevCellKey,
onUpdateSeparators: this._onUpdateSeparators,
onUnmount: this._onCellUnmount,
extraData: extraData,
ref: ref => {
this._cellRefs[key] = ref;
},
};
if (VirtualizedListInjection.useVLOptimization) {
cells.push(
<CellRenderer
{...cellRendererBaseProps}
onLayout={this._onCellLayout}
getItemLayout={getItemLayout}
renderItem={renderItem}
ListItemComponent={ListItemComponent}
debug={debug}
/>,
);
} else {
cells.push(
<CellRenderer
{...cellRendererBaseProps}
experimentalVirtualizedListOpt={false}
onLayout={e => this._onCellLayout(e, key, ii)}
parentProps={this.props}
/>,
);
}
prevCellKey = key;
}
}
Expand Down Expand Up @@ -1269,7 +1289,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
};

_onCellLayout(e, cellKey, index) {
_onCellLayout = (e, cellKey, index): void => {
const layout = e.nativeEvent.layout;
const next = {
offset: this._selectOffset(layout),
Expand Down Expand Up @@ -1302,7 +1322,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {

this._computeBlankness();
this._updateViewableItems(this.props.data);
}
};

_onCellUnmount = (cellKey: string) => {
const curr = this._frames[cellKey];
Expand Down Expand Up @@ -1893,7 +1913,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
}

type CellRendererProps = {
type CellRendererBaseProps = {
CellRendererComponent?: ?React.ComponentType<any>,
ItemSeparatorComponent: ?React.ComponentType<
any | {highlighted: boolean, leadingItem: ?Item},
Expand Down Expand Up @@ -1992,6 +2012,15 @@ class CellRenderer extends React.Component<
this.props.onUnmount(this.props.cellKey);
}

_onLayout = (e): void => {
if (VirtualizedListInjection.useVLOptimization) {
this.props.onLayout &&
this.props.onLayout(e, this.props.cellKey, this.props.index);
} else {
this.props.onLayout && this.props.onLayout(e);
}
};

_renderElement(renderItem, ListItemComponent, item, index) {
if (renderItem && ListItemComponent) {
console.warn(
Expand Down Expand Up @@ -2037,9 +2066,25 @@ class CellRenderer extends React.Component<
item,
index,
inversionStyle,
parentProps,
} = this.props;
const {renderItem, getItemLayout, ListItemComponent} = parentProps;

let ListItemComponent: $PropertyType<OptionalProps, 'ListEmptyComponent'>;
let renderItem: $PropertyType<OptionalProps, 'renderItem'>;
let debug: $PropertyType<OptionalProps, 'debug'>;
let getItemLayout: $PropertyType<OptionalProps, 'getItemLayout'>;
if (this.props.experimentalVirtualizedListOpt === true) {
ListItemComponent = this.props.ListItemComponent;
renderItem = this.props.renderItem;
debug = this.props.debug;
getItemLayout = this.props.getItemLayout;
} else {
const parentProps = this.props.parentProps;
ListItemComponent = parentProps.ListItemComponent;
renderItem = parentProps.renderItem;
debug = parentProps.debug;
getItemLayout = parentProps.getItemLayout;
}

const element = this._renderElement(
renderItem,
ListItemComponent,
Expand Down
19 changes: 19 additions & 0 deletions Libraries/Lists/VirtualizedListInjection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

const experiments = {
useVLOptimization: false,
};

export function setUseVLOptimization() {
experiments.useVLOptimization = true;
}

export default (experiments: {useVLOptimization: boolean});
55 changes: 55 additions & 0 deletions Libraries/Lists/__tests__/VirtualizedList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,61 @@ describe('VirtualizedList', () => {
);
});

it('calls _onCellLayout properly', () => {
const items = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}];
const mock = jest.fn();
const component = ReactTestRenderer.create(
<VirtualizedList
data={items}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
/>,
);
const virtualList: VirtualizedList = component.getInstance();
virtualList._onCellLayout = mock;
component.update(
<VirtualizedList
data={[...items, {key: 'i4'}]}
renderItem={({item}) => <item value={item.key} />}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
/>,
);
const cell = virtualList._cellRefs.i4;
const event = {
nativeEvent: {layout: {x: 0, y: 0, width: 50, height: 50}},
};
cell._onLayout(event);
expect(mock).toHaveBeenCalledWith(event, 'i4', 3);
});

it('handles extraData correctly', () => {
const mock = jest.fn();
const listData = [{key: 'i0'}, {key: 'i1'}, {key: 'i2'}];
const getItem = (data, index) => data[index];
const getItemCount = data => data.length;
const component = ReactTestRenderer.create(
<VirtualizedList
data={listData}
renderItem={mock}
getItem={getItem}
getItemCount={getItemCount}
/>,
);

component.update(
<VirtualizedList
data={listData}
renderItem={mock}
getItem={getItem}
getItemCount={getItemCount}
extraData={{updated: true}}
/>,
);
expect(mock).toHaveBeenCalledTimes(6);
});

it('getScrollRef for case where it returns a ScrollView', () => {
const listRef = React.createRef(null);

Expand Down

0 comments on commit bd4bdfc

Please sign in to comment.