diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index a7c1567b3f607a..cd472121447d45 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -314,6 +314,12 @@ type State = { last: number, }; +type CachedCell = { + component: any, + deps: Array, + used?: boolean, +}; + /** * Default Props Helper Functions * Use the following helper functions for default values @@ -784,8 +790,51 @@ class VirtualizedList extends React.PureComponent { }; } + _cachedCells: Map = new Map(); + _cells = []; + + _markCellsAsUnused() { + this._cachedCells.forEach(cell => { + cell.used = false; + }); + } + + _deleteUnusedCells() { + this._cachedCells.forEach((cell, key) => { + if (!cell.used) { + this._cachedCells.delete(key); + } + }); + } + + _pushCell(key, deps, creator) { + //$FlowFixMe[incompatible-type] + let cell: CachedCell = this._cachedCells.get(key); + let depsChanged = false; + if (!cell || cell.deps.length !== deps.length) { + depsChanged = true; + } else { + for (let i = 0; i < deps.length; i++) { + if (cell.deps[i] !== deps[i]) { + depsChanged = true; + break; + } + } + } + if (depsChanged) { + this._cachedCells.set( + key, + (cell = { + component: creator(), + deps, + }), + ); + } + cell.used = true; + this._cells.push(cell.component); + } + _pushCells( - cells: Array, stickyHeaderIndices: Array, stickyIndicesFromProps: Set, first: number, @@ -809,9 +858,19 @@ class VirtualizedList extends React.PureComponent { const key = this._keyExtractor(item, ii); this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { - stickyHeaderIndices.push(cells.length); + stickyHeaderIndices.push(this._cells.length); } - cells.push( + const deps = [ + CellRendererComponent, + ItemSeparatorComponent, + horizontal, + ii, + inversionStyle, + item, + prevCellKey, + this.props, + ]; + this._pushCell(key, deps, () => ( { ref={ref => { this._cellRefs[key] = ref; }} - />, - ); + /> + )); prevCellKey = key; } } @@ -895,37 +954,46 @@ class VirtualizedList extends React.PureComponent { ? styles.horizontallyInverted : styles.verticallyInverted : null; - const cells = []; + this._cells = []; + this._markCellsAsUnused(); const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices); const stickyHeaderIndices = []; if (ListHeaderComponent) { if (stickyIndicesFromProps.has(0)) { stickyHeaderIndices.push(0); } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); + const deps = [ + inversionStyle, + ListHeaderComponent, + this.props.extraData, + this.props.ListHeaderComponentStyle, + ]; + this._pushCell('$header', deps, () => { + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + return ( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + + ); + }); } const itemCount = this.props.getItemCount(data); if (itemCount > 0) { @@ -937,7 +1005,6 @@ class VirtualizedList extends React.PureComponent { : initialNumToRenderOrDefault(this.props.initialNumToRender) - 1; const {first, last} = this.state; this._pushCells( - cells, stickyHeaderIndices, stickyIndicesFromProps, 0, @@ -958,11 +1025,10 @@ class VirtualizedList extends React.PureComponent { stickyBlock.offset - initBlock.offset - (this.props.initialScrollIndex ? 0 : initBlock.length); - cells.push( + this._cells.push( , ); this._pushCells( - cells, stickyHeaderIndices, stickyIndicesFromProps, ii, @@ -972,7 +1038,7 @@ class VirtualizedList extends React.PureComponent { const trailSpace = this._getFrameMetricsApprox(first).offset - (stickyBlock.offset + stickyBlock.length); - cells.push( + this._cells.push( , ); insertedStickySpacer = true; @@ -985,13 +1051,12 @@ class VirtualizedList extends React.PureComponent { const firstSpace = this._getFrameMetricsApprox(first).offset - (initBlock.offset + initBlock.length); - cells.push( + this._cells.push( , ); } } this._pushCells( - cells, stickyHeaderIndices, stickyIndicesFromProps, firstAfterInitial, @@ -1019,7 +1084,7 @@ class VirtualizedList extends React.PureComponent { endFrame.offset + endFrame.length - (lastFrame.offset + lastFrame.length); - cells.push( + this._cells.push( , ); } @@ -1033,7 +1098,8 @@ class VirtualizedList extends React.PureComponent { // $FlowFixMe[incompatible-type-arg] )): any); - cells.push( + + this._cells.push( React.cloneElement(element, { key: '$empty', onLayout: event => { @@ -1047,30 +1113,38 @@ class VirtualizedList extends React.PureComponent { ); } if (ListFooterComponent) { - const element = React.isValidElement(ListFooterComponent) ? ( - ListFooterComponent - ) : ( - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - - ); - cells.push( - - - { - // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors - element - } - - , - ); + const deps = [ + inversionStyle, + ListFooterComponent, + this.props.extraData, + this.props.ListFooterComponentStyle, + ]; + this._pushCell('$footer', deps, () => { + const element = React.isValidElement(ListFooterComponent) ? ( + ListFooterComponent + ) : ( + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + + ); + return ( + + + { + // $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors + element + } + + + ); + }); } const scrollProps = { ...this.props, @@ -1117,11 +1191,12 @@ class VirtualizedList extends React.PureComponent { { ref: this._captureScrollRef, }, - cells, + this._cells, )} ); let ret = innerRet; + this._deleteUnusedCells(); if (__DEV__) { ret = ( @@ -1931,22 +2006,18 @@ type CellRendererProps = { }; type CellRendererState = { - separatorProps: $ReadOnly<{| - highlighted: boolean, - leadingItem: ?Item, - |}>, + highlighted: boolean, + leadingItem: ?Item, ... }; -class CellRenderer extends React.Component< +class CellRenderer extends React.PureComponent< CellRendererProps, CellRendererState, > { state = { - separatorProps: { - highlighted: false, - leadingItem: this.props.item, - }, + highlighted: false, + leadingItem: this.props.item, }; static getDerivedStateFromProps( @@ -1954,10 +2025,8 @@ class CellRenderer extends React.Component< prevState: CellRendererState, ): ?CellRendererState { return { - separatorProps: { - ...prevState.separatorProps, - leadingItem: props.item, - }, + ...prevState, + leadingItem: props.item, }; } @@ -1987,7 +2056,8 @@ class CellRenderer extends React.Component< updateSeparatorProps(newProps: Object) { this.setState(state => ({ - separatorProps: {...state.separatorProps, ...newProps}, + ...state, + ...newProps, })); } @@ -2060,7 +2130,7 @@ class CellRenderer extends React.Component< // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and // called explicitly by `ScrollViewStickyHeader`. const itemSeparator = ItemSeparatorComponent && ( - + ); const cellStyle = inversionStyle ? horizontal