Remove dependence on VirtualizedList listKey prop

Summary:
Following up on the previous diff to remove the only usage of `listKey` for persistent association, we can remove the need for a manual `listKey`, and instead rely on per-instance association via refs.

A followup change will remove the existing usages.

Changelog:
[General][Removed] - Remove VirtualizedList `listKey` prop

Reviewed By: p-sun

Differential Revision: D39466677

fbshipit-source-id: 6b49f45c987fff9836918ba833fbb16f24414ff8
This commit is contained in:
Nick Gerleman
2022-09-19 19:25:09 -07:00
committed by Facebook GitHub Bot
parent c5217f199d
commit 010da67bef
6 changed files with 129 additions and 324 deletions
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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
* @format
*/
import invariant from 'invariant';
export default class ChildListCollection<TList> {
_cellKeyToChildren: Map<string, Set<TList>> = new Map();
_childrenToCellKey: Map<TList, string> = new Map();
add(list: TList, cellKey: string): void {
invariant(
!this._childrenToCellKey.has(list),
'Trying to add already present child list',
);
const cellLists = this._cellKeyToChildren.get(cellKey) ?? new Set();
cellLists.add(list);
this._cellKeyToChildren.set(cellKey, cellLists);
this._childrenToCellKey.set(list, cellKey);
}
remove(list: TList): void {
const cellKey = this._childrenToCellKey.get(list);
invariant(cellKey != null, 'Trying to remove non-present child list');
this._childrenToCellKey.delete(list);
const cellLists = this._cellKeyToChildren.get(cellKey);
invariant(cellLists, '_cellKeyToChildren should contain cellKey');
cellLists.delete(list);
if (cellLists.size === 0) {
this._cellKeyToChildren.delete(cellKey);
}
}
forEach(fn: TList => void): void {
for (const listSet of this._cellKeyToChildren.values()) {
for (const list of listSet) {
fn(list);
}
}
}
forEachInCell(cellKey: string, fn: TList => void): void {
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
for (const list of listSet) {
fn(list);
}
}
anyInCell(cellKey: string, fn: TList => boolean): boolean {
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
for (const list of listSet) {
if (fn(list)) {
return true;
}
}
return false;
}
size(): number {
return this._childrenToCellKey.size;
}
}
+27 -116
View File
@@ -24,7 +24,6 @@ import type {
export type {RenderItemProps, RenderItemType, Separators};
import {
type ListDebugInfo,
VirtualizedListCellContextProvider,
VirtualizedListContext,
VirtualizedListContextProvider,
@@ -35,6 +34,7 @@ import {
} from './VirtualizeUtils';
import * as VirtualizedListInjection from './VirtualizedListInjection';
import * as React from 'react';
import ChildListCollection from './ChildListCollection';
const RefreshControl = require('../Components/RefreshControl/RefreshControl');
const ScrollView = require('../Components/ScrollView/ScrollView');
@@ -294,7 +294,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
recordInteraction() {
this._nestedChildLists.forEach(childList => {
childList && childList.recordInteraction();
childList.recordInteraction();
});
this._viewabilityTuples.forEach(t => {
t.viewabilityHelper.recordInteraction();
@@ -349,19 +349,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
return this.context?.cellKey || 'rootList';
}
_getListKey(): string {
return this.props.listKey || this._getCellKey();
}
_getDebugInfo(): ListDebugInfo {
return {
listKey: this._getListKey(),
cellKey: this._getCellKey(),
horizontal: horizontalOrDefault(this.props.horizontal),
parent: this.context?.debugInfo,
};
}
// $FlowFixMe[missing-local-annot]
_getScrollMetrics = () => {
return this._scrollMetrics;
@@ -382,41 +369,20 @@ class VirtualizedList extends React.PureComponent<Props, State> {
_registerAsNestedChild = (childList: {
cellKey: string,
key: string,
ref: React.ElementRef<typeof React.Component>,
parentDebugInfo: ListDebugInfo,
...
}): void => {
const specificRef = ((childList.ref: any): VirtualizedList);
// Register the mapping between this child key and the cellKey for its cell
const childListsInCell =
this._cellKeysToChildListKeys.get(childList.cellKey) || new Set();
childListsInCell.add(childList.key);
this._cellKeysToChildListKeys.set(childList.cellKey, childListsInCell);
const existingChildData = this._nestedChildLists.get(childList.key);
if (existingChildData) {
console.error(
'A VirtualizedList contains a cell which itself contains ' +
'more than one VirtualizedList of the same orientation as the parent ' +
'list. You must pass a unique listKey prop to each sibling list.\n\n' +
describeNestedLists({
...childList,
ref: specificRef,
// We're called from the child's componentDidMount, so it's safe to
// read the child's props here (albeit weird).
horizontal: !!specificRef.props.horizontal,
}),
);
}
this._nestedChildLists.set(childList.key, specificRef);
const listRef = ((childList.ref: any): VirtualizedList);
this._nestedChildLists.add(listRef, childList.cellKey);
if (this._hasInteracted) {
specificRef.recordInteraction();
listRef.recordInteraction();
}
};
_unregisterAsNestedChild = (childList: {key: string, ...}): void => {
this._nestedChildLists.delete(childList.key);
_unregisterAsNestedChild = (childList: {
ref: React.ElementRef<typeof React.Component>,
}): void => {
const listRef = ((childList.ref: any): VirtualizedList);
this._nestedChildLists.remove(listRef);
};
state: State;
@@ -476,28 +442,15 @@ class VirtualizedList extends React.PureComponent<Props, State> {
componentDidMount() {
if (this._isNestedWithSameOrientation()) {
this.context.registerAsNestedChild({
cellKey: this._getCellKey(),
key: this._getListKey(),
ref: this,
// NOTE: When the child mounts (here) it's not necessarily safe to read
// the parent's props. This is why we explicitly propagate debugInfo
// "down" via context and "up" again via this method call on the
// parent.
parentDebugInfo: this.context.debugInfo,
cellKey: this.context.cellKey,
});
}
}
componentWillUnmount() {
if (this._isNestedWithSameOrientation()) {
this.context.unregisterAsNestedChild({
key: this._getListKey(),
state: {
first: this.state.first,
last: this.state.last,
frames: this._frames,
},
});
this.context.unregisterAsNestedChild({ref: this});
}
this._updateViewableItems(null);
this._updateCellsToRenderBatcher.dispose({abort: true});
@@ -851,7 +804,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
getOutermostParentListRef: this._getOutermostParentListRef,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild,
debugInfo: this._getDebugInfo(),
}}>
{React.cloneElement(
(
@@ -927,8 +879,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
_averageCellLength = 0;
// Maps a cell key to the set of keys for all outermost child lists within that cell
_cellKeysToChildListKeys: Map<string, Set<string>> = new Map();
_cellRefs: {[string]: null | CellRenderer} = {};
_fillRateHelper: FillRateHelper;
_frames: {
@@ -949,7 +899,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
_hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update
_highestMeasuredFrameIndex = 0;
_indicesToKeys: Map<number, string> = new Map();
_nestedChildLists: Map<string, VirtualizedList> = new Map();
_nestedChildLists: ChildListCollection<VirtualizedList> =
new ChildListCollection();
_offsetFromParentVirtualizedList: number = 0;
_prevParentOffset: number = 0;
// $FlowFixMe[missing-local-annot]
@@ -1067,13 +1018,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
};
_triggerRemeasureForChildListsInCell(cellKey: string): void {
const childListKeys = this._cellKeysToChildListKeys.get(cellKey);
if (childListKeys) {
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
childList && childList.measureLayoutRelativeToContainingList();
}
}
this._nestedChildLists.forEachInCell(cellKey, childList => {
childList.measureLayoutRelativeToContainingList();
});
}
measureLayoutRelativeToContainingList(): void {
@@ -1107,14 +1054,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// If metrics of the scrollView changed, then we triggered remeasure for child list
// to ensure VirtualizedList has the right information.
this._cellKeysToChildListKeys.forEach(childListKeys => {
if (childListKeys) {
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
childList &&
childList.measureLayoutRelativeToContainingList();
}
}
this._nestedChildLists.forEach(childList => {
childList.measureLayoutRelativeToContainingList();
});
}
},
@@ -1547,7 +1488,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
last: Math.min(state.last + renderAhead, getItemCount(data) - 1),
};
}
if (newState && this._nestedChildLists.size > 0) {
if (newState && this._nestedChildLists.size() > 0) {
const newFirst = newState.first;
const newLast = newState.last;
// If some cell in the new state has a child list in it, we should only render
@@ -1556,21 +1497,16 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// their items.
for (let ii = newFirst; ii <= newLast; ii++) {
const cellKeyForIndex = this._indicesToKeys.get(ii);
const childListKeys =
cellKeyForIndex &&
this._cellKeysToChildListKeys.get(cellKeyForIndex);
if (!childListKeys) {
if (cellKeyForIndex == null) {
continue;
}
let someChildHasMore = false;
// For each cell, need to check whether any child list in it has more elements to render
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
if (childList && childList.hasMore()) {
someChildHasMore = true;
break;
}
}
const someChildHasMore = this._nestedChildLists.anyInCell(
cellKeyForIndex,
childList => childList.hasMore(),
);
if (someChildHasMore) {
newState.last = ii;
break;
@@ -1879,31 +1815,6 @@ class CellRenderer extends React.Component<
}
}
function describeNestedLists(childList: {
+cellKey: string,
+key: string,
+ref: VirtualizedList,
+parentDebugInfo: ListDebugInfo,
+horizontal: boolean,
...
}) {
let trace =
'VirtualizedList trace:\n' +
` Child (${childList.horizontal ? 'horizontal' : 'vertical'}):\n` +
` listKey: ${childList.key}\n` +
` cellKey: ${childList.cellKey}`;
let debugInfo: ?ListDebugInfo = childList.parentDebugInfo;
while (debugInfo) {
trace +=
`\n Parent (${debugInfo.horizontal ? 'horizontal' : 'vertical'}):\n` +
` listKey: ${debugInfo.listKey}\n` +
` cellKey: ${debugInfo.cellKey}`;
debugInfo = debugInfo.parent;
}
return trace;
}
const styles = StyleSheet.create({
verticallyInverted: {
transform: [{scaleY: -1}],
+2 -27
View File
@@ -4,25 +4,13 @@
* 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
* @flow strict
* @format
*/
import * as React from 'react';
import {useMemo, useContext} from 'react';
// Data propagated through nested lists (regardless of orientation) that is
// useful for producing diagnostics for usage errors involving nesting (e.g
// missing/duplicate keys).
export type ListDebugInfo = $ReadOnly<{
cellKey: string,
listKey: string,
parent: ?ListDebugInfo,
// We include all ancestors regardless of orientation, so this is not always
// identical to the child's orientation.
horizontal: boolean,
}>;
type Context = $ReadOnly<{
cellKey: ?string,
getScrollMetrics: () => {
@@ -39,14 +27,11 @@ type Context = $ReadOnly<{
getOutermostParentListRef: () => React.ElementRef<typeof React.Component>,
registerAsNestedChild: ({
cellKey: string,
key: string,
ref: React.ElementRef<typeof React.Component>,
parentDebugInfo: ListDebugInfo,
}) => void,
unregisterAsNestedChild: ({
key: string,
ref: React.ElementRef<typeof React.Component>,
}) => void,
debugInfo: ListDebugInfo,
}>;
export const VirtualizedListContext: React.Context<?Context> =
@@ -89,12 +74,6 @@ export function VirtualizedListContextProvider({
getOutermostParentListRef: value.getOutermostParentListRef,
registerAsNestedChild: value.registerAsNestedChild,
unregisterAsNestedChild: value.unregisterAsNestedChild,
debugInfo: {
cellKey: value.debugInfo.cellKey,
horizontal: value.debugInfo.horizontal,
listKey: value.debugInfo.listKey,
parent: value.debugInfo.parent,
},
}),
[
value.getScrollMetrics,
@@ -102,10 +81,6 @@ export function VirtualizedListContextProvider({
value.getOutermostParentListRef,
value.registerAsNestedChild,
value.unregisterAsNestedChild,
value.debugInfo.cellKey,
value.debugInfo.horizontal,
value.debugInfo.listKey,
value.debugInfo.parent,
],
);
return (
+2
View File
@@ -167,6 +167,8 @@ type OptionalProps = {|
* A unique identifier for this list. If there are multiple VirtualizedLists at the same level of
* nesting within another VirtualizedList, this key is necessary for virtualization to
* work properly.
*
* @deprecated no longer used/required
*/
listKey?: string,
/**
+26 -113
View File
@@ -29,7 +29,6 @@ import type {
export type {RenderItemProps, RenderItemType, Separators};
import {
type ListDebugInfo,
VirtualizedListCellContextProvider,
VirtualizedListContext,
VirtualizedListContextProvider,
@@ -43,6 +42,7 @@ import * as React from 'react';
import {CellRenderMask} from './CellRenderMask';
import clamp from '../Utilities/clamp';
import StateSafePureComponent from './StateSafePureComponent';
import ChildListCollection from './ChildListCollection';
const RefreshControl = require('../Components/RefreshControl/RefreshControl');
const ScrollView = require('../Components/ScrollView/ScrollView');
@@ -370,19 +370,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
return this.context?.cellKey || 'rootList';
}
_getListKey(): string {
return this.props.listKey || this._getCellKey();
}
_getDebugInfo(): ListDebugInfo {
return {
listKey: this._getListKey(),
cellKey: this._getCellKey(),
horizontal: horizontalOrDefault(this.props.horizontal),
parent: this.context?.debugInfo,
};
}
// $FlowFixMe[missing-local-annot]
_getScrollMetrics = () => {
return this._scrollMetrics;
@@ -403,41 +390,20 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
_registerAsNestedChild = (childList: {
cellKey: string,
key: string,
ref: React.ElementRef<typeof React.Component>,
parentDebugInfo: ListDebugInfo,
...
}): void => {
const specificRef = ((childList.ref: any): VirtualizedList);
// Register the mapping between this child key and the cellKey for its cell
const childListsInCell =
this._cellKeysToChildListKeys.get(childList.cellKey) || new Set();
childListsInCell.add(childList.key);
this._cellKeysToChildListKeys.set(childList.cellKey, childListsInCell);
const existingChildData = this._nestedChildLists.get(childList.key);
if (existingChildData) {
console.error(
'A VirtualizedList contains a cell which itself contains ' +
'more than one VirtualizedList of the same orientation as the parent ' +
'list. You must pass a unique listKey prop to each sibling list.\n\n' +
describeNestedLists({
...childList,
ref: specificRef,
// We're called from the child's componentDidMount, so it's safe to
// read the child's props here (albeit weird).
horizontal: !!specificRef.props.horizontal,
}),
);
}
this._nestedChildLists.set(childList.key, specificRef);
const listRef = ((childList.ref: any): VirtualizedList);
this._nestedChildLists.add(listRef, childList.cellKey);
if (this._hasInteracted) {
specificRef.recordInteraction();
listRef.recordInteraction();
}
};
_unregisterAsNestedChild = (childList: {key: string, ...}): void => {
this._nestedChildLists.delete(childList.key);
_unregisterAsNestedChild = (childList: {
ref: React.ElementRef<typeof React.Component>,
}): void => {
const listRef = ((childList.ref: any): VirtualizedList);
this._nestedChildLists.remove(listRef);
};
state: State;
@@ -640,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
);
}
if (this._nestedChildLists.size > 0) {
if (this._nestedChildLists.size() > 0) {
// If some cell in the new state has a child list in it, we should only render
// up through that item, so that we give that list a chance to render.
// Otherwise there's churn from multiple child lists mounting and un-mounting
@@ -661,17 +627,13 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
_findFirstChildWithMore(first: number, last: number): number | null {
for (let ii = first; ii <= last; ii++) {
const cellKeyForIndex = this._indicesToKeys.get(ii);
const childListKeys =
cellKeyForIndex && this._cellKeysToChildListKeys.get(cellKeyForIndex);
if (!childListKeys) {
continue;
}
// For each cell, need to check whether any child list in it has more elements to render
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
if (childList && childList.hasMore()) {
return ii;
}
if (
cellKeyForIndex != null &&
this._nestedChildLists.anyInCell(cellKeyForIndex, childList =>
childList.hasMore(),
)
) {
return ii;
}
}
@@ -681,27 +643,15 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
componentDidMount() {
if (this._isNestedWithSameOrientation()) {
this.context.registerAsNestedChild({
cellKey: this._getCellKey(),
key: this._getListKey(),
ref: this,
// NOTE: When the child mounts (here) it's not necessarily safe to read
// the parent's props. This is why we explicitly propagate debugInfo
// "down" via context and "up" again via this method call on the
// parent.
parentDebugInfo: this.context.debugInfo,
cellKey: this.context.cellKey,
});
}
}
componentWillUnmount() {
if (this._isNestedWithSameOrientation()) {
this.context.unregisterAsNestedChild({
key: this._getListKey(),
state: {
...this.state,
frames: this._frames,
},
});
this.context.unregisterAsNestedChild({ref: this});
}
this._updateCellsToRenderBatcher.dispose({abort: true});
this._viewabilityTuples.forEach(tuple => {
@@ -1054,7 +1004,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
getOutermostParentListRef: this._getOutermostParentListRef,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild,
debugInfo: this._getDebugInfo(),
}}>
{React.cloneElement(
(
@@ -1131,8 +1080,6 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
}
_averageCellLength = 0;
// Maps a cell key to the set of keys for all outermost child lists within that cell
_cellKeysToChildListKeys: Map<string, Set<string>> = new Map();
_cellRefs: {[string]: null | CellRenderer} = {};
_fillRateHelper: FillRateHelper;
_frames: {
@@ -1154,7 +1101,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
_highestMeasuredFrameIndex = 0;
_indicesToKeys: Map<number, string> = new Map();
_lastFocusedCellKey: ?string = null;
_nestedChildLists: Map<string, VirtualizedList> = new Map();
_nestedChildLists: ChildListCollection<VirtualizedList> =
new ChildListCollection();
_offsetFromParentVirtualizedList: number = 0;
_prevParentOffset: number = 0;
// $FlowFixMe[missing-local-annot]
@@ -1288,13 +1236,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
};
_triggerRemeasureForChildListsInCell(cellKey: string): void {
const childListKeys = this._cellKeysToChildListKeys.get(cellKey);
if (childListKeys) {
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
childList && childList.measureLayoutRelativeToContainingList();
}
}
this._nestedChildLists.forEachInCell(cellKey, childList => {
childList.measureLayoutRelativeToContainingList();
});
}
measureLayoutRelativeToContainingList(): void {
@@ -1328,14 +1272,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
// If metrics of the scrollView changed, then we triggered remeasure for child list
// to ensure VirtualizedList has the right information.
this._cellKeysToChildListKeys.forEach(childListKeys => {
if (childListKeys) {
for (let childKey of childListKeys) {
const childList = this._nestedChildLists.get(childKey);
childList &&
childList.measureLayoutRelativeToContainingList();
}
}
this._nestedChildLists.forEach(childList => {
childList.measureLayoutRelativeToContainingList();
});
}
},
@@ -2128,31 +2066,6 @@ class CellRenderer extends React.Component<
}
}
function describeNestedLists(childList: {
+cellKey: string,
+key: string,
+ref: VirtualizedList,
+parentDebugInfo: ListDebugInfo,
+horizontal: boolean,
...
}) {
let trace =
'VirtualizedList trace:\n' +
` Child (${childList.horizontal ? 'horizontal' : 'vertical'}):\n` +
` listKey: ${childList.key}\n` +
` cellKey: ${childList.cellKey}`;
let debugInfo: ?ListDebugInfo = childList.parentDebugInfo;
while (debugInfo) {
trace +=
`\n Parent (${debugInfo.horizontal ? 'horizontal' : 'vertical'}):\n` +
` listKey: ${debugInfo.listKey}\n` +
` cellKey: ${debugInfo.cellKey}`;
debugInfo = debugInfo.parent;
}
return trace;
}
const styles = StyleSheet.create({
verticallyInverted: {
transform: [{scaleY: -1}],
@@ -420,74 +420,6 @@ describe('VirtualizedList', () => {
expect(onEndReached).toHaveBeenCalled();
});
it('provides a trace when a listKey collision occurs', () => {
const errors = [];
jest.spyOn(console, 'error').mockImplementation((...args) => {
// Silence the DEV-only React error boundary warning.
if ((args[0] || '').startsWith('The above error occurred in the ')) {
return;
}
errors.push(args);
});
const commonProps = {
data: [{key: 'cell0'}],
getItem: (data, index) => data[index],
getItemCount: data => data.length,
renderItem: ({item}) => <item value={item.key} />,
};
try {
ReactTestRenderer.create(
<VirtualizedList
{...commonProps}
horizontal={false}
listKey="level0"
renderItem={() => (
<VirtualizedList
{...commonProps}
horizontal={true}
listKey="level1"
renderItem={() => (
<>
{/* Force a collision */}
<VirtualizedList
{...commonProps}
horizontal={true}
listKey="level2"
/>
<VirtualizedList
{...commonProps}
horizontal={true}
listKey="level2"
/>
</>
)}
/>
)}
/>,
);
expect(errors).toMatchInlineSnapshot(`
Array [
Array [
"A VirtualizedList contains a cell which itself contains more than one VirtualizedList of the same orientation as the parent list. You must pass a unique listKey prop to each sibling list.
VirtualizedList trace:
Child (horizontal):
listKey: level2
cellKey: cell0
Parent (horizontal):
listKey: level1
cellKey: cell0
Parent (vertical):
listKey: level0
cellKey: rootList",
],
]
`);
} finally {
console.error.mockRestore();
}
});
it('throws if using scrollToIndex with index less than 0', () => {
const component = ReactTestRenderer.create(
<VirtualizedList