diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 56364823f06d49..fd72655a170ab0 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -234,6 +234,65 @@ type ChildListState = { type State = {first: number, last: number}; +/** + * Performs equality by iterating through keys on an object and returning false + * when any key has values which are not strictly equal between the arguments. + * Returns true when the values of all keys are strictly equal. + */ +function shallowEqual(objA: Object, objB: Object, exceptions = []) { + if (is(objA, objB)) { + return true; + } + + if ( + typeof objA !== 'object' || + objA === null || + typeof objB !== 'object' || + objB === null + ) { + return false; + } + + var keysA = Object.keys(objA); + var keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + // Test for A's keys different from B. + for (let i = 0; i < keysA.length; i++) { + if (exceptions.findIndex(ex => ex === keysA[i]) !== -1) { + continue; + } + if ( + !hasOwnProperty.call(objB, keysA[i]) || + !is(objA[keysA[i]], objB[keysA[i]]) + ) { + return false; + } + } + + return true; +} + +/** + * inlined Object.is polyfill to avoid requiring consumers ship their own + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + */ +function is(x, y) { + // SameValue algorithm + if (x === y) { + // Steps 1-5, 7-10 + // Steps 6.b-6.e: +0 != -0 + // Added the nonzero y check to make Flow happy, but it is redundant + return x !== 0 || y !== 0 || 1 / x === 1 / y; + } else { + // Step 6.a: NaN == NaN + return x !== x && y !== y; + } +} + /** * Base implementation for the more convenient [``](/react-native/docs/flatlist.html) * and [``](/react-native/docs/sectionlist.html) components, which are also better @@ -675,6 +734,12 @@ class VirtualizedList extends React.PureComponent { if (stickyIndicesFromProps.has(ii + stickyOffset)) { stickyHeaderIndices.push(cells.length); } + // remove all components from parentProps so that these components don't cause the CellRenderer to re-render. + const parentProps: Props = {...this.props}; + delete parentProps.ItemSeparatorComponent; + delete parentProps.ListEmptyComponent; + delete parentProps.ListFooterComponent; + delete parentProps.ListHeaderComponent; cells.push( { key={key} prevCellKey={prevCellKey} onUpdateSeparators={this._onUpdateSeparators} - onLayout={e => this._onCellLayout(e, key, ii)} + onLayout={this._onCellLayout} onUnmount={this._onCellUnmount} - parentProps={this.props} + parentProps={parentProps} ref={ref => { this._cellRefs[key] = ref; }} @@ -1046,7 +1111,7 @@ class VirtualizedList extends React.PureComponent { } }; - _onCellLayout(e, cellKey, index) { + _onCellLayout = (e, cellKey, index): void => { const layout = e.nativeEvent.layout; const next = { offset: this._selectOffset(layout), @@ -1086,7 +1151,7 @@ class VirtualizedList extends React.PureComponent { } this._computeBlankness(); - } + }; _onCellUnmount = (cellKey: string) => { const curr = this._frames[cellKey]; @@ -1604,7 +1669,7 @@ class CellRenderer extends React.Component< index: number, inversionStyle: ?DangerouslyImpreciseStyleProp, item: Item, - onLayout: (event: Object) => void, // This is extracted by ScrollViewStickyHeader + onLayout: (event: Object, key: string, index: number) => void, // This is extracted by ScrollViewStickyHeader onUnmount: (cellKey: string) => void, onUpdateSeparators: (cellKeys: Array, props: Object) => void, parentProps: { @@ -1670,6 +1735,20 @@ class CellRenderer extends React.Component< this.props.onUnmount(this.props.cellKey); } + _onLayout = (e): void => + this.props.onLayout && + this.props.onLayout(e, this.props.cellKey, this.props.index); + + shouldComponentUpdate(nextProps, nextState) { + if (!shallowEqual(this.props, nextProps, ['parentProps'])) { + return true; + } + if (!shallowEqual(this.props.parentProps, nextProps.parentProps)) { + return true; + } + return !shallowEqual(this.state, nextState); + } + render() { const { CellRendererComponent, @@ -1694,7 +1773,7 @@ class CellRenderer extends React.Component< * comment and run Flow. */ getItemLayout && !parentProps.debug && !fillRateHelper.enabled() ? undefined - : this.props.onLayout; + : this._onLayout; // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and // called explicitly by `ScrollViewStickyHeader`. const itemSeparator = ItemSeparatorComponent && ( diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index 14aec9140f1195..1b94791cd11d3d 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -218,4 +218,33 @@ describe('VirtualizedList', () => { }), ); }); + + it('calls _onCellLayout properly', () => { + const items = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]; + const mock = jest.fn(); + const component = ReactTestRenderer.create( + } + getItem={(data, index) => data[index]} + getItemCount={data => data.length} + />, + ); + const virtualList: VirtualizedList = component.getInstance(); + virtualList._onCellLayout = mock; + component.update( + } + 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); + }); });