VirtualizedList: Migrate to React.Context

Summary:
Migrates `VirtualizedList` off legacy context by creating `VirtualizedListContext`.

Changelog:
[General][Changed] - Migrated `virtualizedList` legacy context to `React.Context`.

Reviewed By: TheSavior

Differential Revision: D21370882

fbshipit-source-id: 2fa99ee0bc0e6b747a2d3fe7c66ee402c6b9c5af
This commit is contained in:
Tim Yung
2020-05-04 01:00:06 -07:00
committed by Facebook GitHub Bot
parent 9cdc19a936
commit 7bd694fc6f
3 changed files with 226 additions and 230 deletions
+63 -211
View File
@@ -13,7 +13,6 @@
const Batchinator = require('../Interaction/Batchinator');
const FillRateHelper = require('./FillRateHelper');
const PropTypes = require('prop-types');
const React = require('react');
const ReactNative = require('../Renderer/shims/ReactNative');
const RefreshControl = require('../Components/RefreshControl/RefreshControl');
const ScrollView = require('../Components/ScrollView/ScrollView');
@@ -28,6 +27,7 @@ const warning = require('fbjs/lib/warning');
const {computeWindowedRenderLimits} = require('./VirtualizeUtils');
import * as React from 'react';
import type {ScrollResponderType} from '../Components/ScrollView/ScrollView';
import type {ViewStyleProp} from '../StyleSheet/StyleSheet';
import type {
@@ -35,6 +35,13 @@ import type {
ViewToken,
ViewabilityConfigCallbackPair,
} from './ViewabilityHelper';
import {
VirtualizedListCellContextProvider,
VirtualizedListContext,
VirtualizedListContextProvider,
type ChildListState,
type ListDebugInfo,
} from './VirtualizedListContext.js';
type Item = any;
@@ -315,37 +322,9 @@ type DefaultProps = {|
let _usedIndexForKey = false;
let _keylessItemComponentName: string = '';
type Frame = {
offset: number,
length: number,
index: number,
inLayout: boolean,
...
};
type ChildListState = {
first: number,
last: number,
frames: {[key: number]: Frame, ...},
...
};
type State = {
first: number,
last: number,
...
};
// Data propagated through nested lists (regardless of orientation) that is
// useful for producing diagnostics for usage errors involving nesting (e.g
// missing/duplicate keys).
type ListDebugInfo = {
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,
};
/**
@@ -377,7 +356,7 @@ type ListDebugInfo = {
*
*/
class VirtualizedList extends React.PureComponent<Props, State> {
props: Props;
static contextType: typeof VirtualizedListContext = VirtualizedListContext;
// scrollToEnd may be janky without getItemLayout prop
scrollToEnd(params?: ?{animated?: ?boolean, ...}) {
@@ -582,111 +561,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
windowSize: 21, // multiples of length
};
static contextTypes:
| any
| {|
virtualizedCell: {|
cellKey: React$PropType$Primitive<string>,
|},
virtualizedList: {|
getScrollMetrics: React$PropType$Primitive<Function>,
horizontal: React$PropType$Primitive<boolean>,
getOutermostParentListRef: React$PropType$Primitive<Function>,
getNestedChildState: React$PropType$Primitive<Function>,
registerAsNestedChild: React$PropType$Primitive<Function>,
unregisterAsNestedChild: React$PropType$Primitive<Function>,
debugInfo: {|
listKey: React$PropType$Primitive<string>,
cellKey: React$PropType$Primitive<string>,
|},
|},
|} = {
virtualizedCell: PropTypes.shape({
cellKey: PropTypes.string,
}),
virtualizedList: PropTypes.shape({
getScrollMetrics: PropTypes.func,
horizontal: PropTypes.bool,
getOutermostParentListRef: PropTypes.func,
getNestedChildState: PropTypes.func,
registerAsNestedChild: PropTypes.func,
unregisterAsNestedChild: PropTypes.func,
debugInfo: PropTypes.shape({
listKey: PropTypes.string,
cellKey: PropTypes.string,
}),
}),
};
static childContextTypes:
| any
| {|
getScrollMetrics: React$PropType$Primitive<Function>,
horizontal: React$PropType$Primitive<boolean>,
getOutermostParentListRef: React$PropType$Primitive<Function>,
getNestedChildState: React$PropType$Primitive<Function>,
registerAsNestedChild: React$PropType$Primitive<Function>,
unregisterAsNestedChild: React$PropType$Primitive<Function>,
|} = {
virtualizedList: PropTypes.shape({
getScrollMetrics: PropTypes.func,
horizontal: PropTypes.bool,
getOutermostParentListRef: PropTypes.func,
getNestedChildState: PropTypes.func,
registerAsNestedChild: PropTypes.func,
unregisterAsNestedChild: PropTypes.func,
}),
};
getChildContext(): {|
virtualizedList: {
getScrollMetrics: () => {
contentLength: number,
dOffset: number,
dt: number,
offset: number,
timestamp: number,
velocity: number,
visibleLength: number,
...
},
horizontal: ?boolean,
getOutermostParentListRef: Function,
getNestedChildState: string => ?ChildListState,
registerAsNestedChild: ({
cellKey: string,
key: string,
ref: VirtualizedList,
parentDebugInfo: ListDebugInfo,
...
}) => ?ChildListState,
unregisterAsNestedChild: ({
key: string,
state: ChildListState,
...
}) => void,
debugInfo: ListDebugInfo,
...
},
|} {
return {
virtualizedList: {
getScrollMetrics: this._getScrollMetrics,
horizontal: this.props.horizontal,
getOutermostParentListRef: this._getOutermostParentListRef,
getNestedChildState: this._getNestedChildState,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild,
debugInfo: this._getDebugInfo(),
},
};
}
_getCellKey(): string {
return (
(this.context.virtualizedCell && this.context.virtualizedCell.cellKey) ||
'rootList'
);
return this.context?.cellKey || 'rootList';
}
_getListKey(): string {
@@ -698,9 +574,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
listKey: this._getListKey(),
cellKey: this._getCellKey(),
horizontal: !!this.props.horizontal,
parent: this.context.virtualizedList
? this.context.virtualizedList.debugInfo
: null,
parent: this.context?.debugInfo,
};
}
@@ -714,7 +588,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
_getOutermostParentListRef = () => {
if (this._isNestedWithSameOrientation()) {
return this.context.virtualizedList.getOutermostParentListRef();
return this.context.getOutermostParentListRef();
} else {
return this;
}
@@ -774,8 +648,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
state: State;
constructor(props: Props, context: Object) {
super(props, context);
constructor(props: Props) {
super(props);
invariant(
// $FlowFixMe
!props.onScroll || !props.onScroll.__isNative,
@@ -818,9 +692,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
};
if (this._isNestedWithSameOrientation()) {
const storedState = this.context.virtualizedList.getNestedChildState(
this._getListKey(),
);
const storedState = this.context.getNestedChildState(this._getListKey());
if (storedState) {
initialState = storedState;
this.state = storedState;
@@ -833,7 +705,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
componentDidMount() {
if (this._isNestedWithSameOrientation()) {
this.context.virtualizedList.registerAsNestedChild({
this.context.registerAsNestedChild({
cellKey: this._getCellKey(),
key: this._getListKey(),
ref: this,
@@ -841,14 +713,14 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// 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.virtualizedList.debugInfo,
parentDebugInfo: this.context.debugInfo,
});
}
}
componentWillUnmount() {
if (this._isNestedWithSameOrientation()) {
this.context.virtualizedList.unregisterAsNestedChild({
this.context.unregisterAsNestedChild({
key: this._getListKey(),
state: {
first: this.state.first,
@@ -943,7 +815,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
_isNestedWithSameOrientation(): boolean {
const nestedContext = this.context.virtualizedList;
const nestedContext = this.context;
return !!(
nestedContext && !!nestedContext.horizontal === !!this.props.horizontal
);
@@ -984,7 +856,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<ListHeaderComponent />
);
cells.push(
<VirtualizedCellWrapper
<VirtualizedListCellContextProvider
cellKey={this._getCellKey() + '-header'}
key="$header">
<View
@@ -998,7 +870,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
element
}
</View>
</VirtualizedCellWrapper>,
</VirtualizedListCellContextProvider>,
);
}
const itemCount = this.props.getItemCount(data);
@@ -1139,7 +1011,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
<ListFooterComponent />
);
cells.push(
<VirtualizedCellWrapper
<VirtualizedListCellContextProvider
cellKey={this._getFooterCellKey()}
key="$footer">
<View
@@ -1153,7 +1025,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
element
}
</View>
</VirtualizedCellWrapper>,
</VirtualizedListCellContextProvider>,
);
}
const scrollProps = {
@@ -1178,14 +1050,29 @@ class VirtualizedList extends React.PureComponent<Props, State> {
this._hasMore =
this.state.last < this.props.getItemCount(this.props.data) - 1;
const innerRet = React.cloneElement(
(this.props.renderScrollComponent || this._defaultRenderScrollComponent)(
scrollProps,
),
{
ref: this._captureScrollRef,
},
cells,
const innerRet = (
<VirtualizedListContextProvider
value={{
cellKey: null,
getScrollMetrics: this._getScrollMetrics,
horizontal: this.props.horizontal,
getOutermostParentListRef: this._getOutermostParentListRef,
getNestedChildState: this._getNestedChildState,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild,
debugInfo: this._getDebugInfo(),
}}>
{React.cloneElement(
(
this.props.renderScrollComponent ||
this._defaultRenderScrollComponent
)(scrollProps),
{
ref: this._captureScrollRef,
},
cells,
)}
</VirtualizedListContextProvider>
);
let ret = innerRet;
if (__DEV__) {
@@ -1196,7 +1083,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
scrollContext != null &&
!scrollContext.horizontal === !this.props.horizontal &&
!this._hasWarned.nesting &&
this.context.virtualizedList == null
this.context == null
) {
// TODO (T46547044): use React.warn once 16.9 is sync'd: https://github.com/facebook/react/pull/15170
console.warn(
@@ -1401,7 +1288,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// We are assuming that getOutermostParentListRef().getScrollRef()
// is a non-null reference to a ScrollView
this._scrollRef.measureLayout(
this.context.virtualizedList.getOutermostParentListRef().getScrollRef(),
this.context.getOutermostParentListRef().getScrollRef(),
(x, y, width, height) => {
this._offsetFromParentVirtualizedList = this._selectOffset({x, y});
this._scrollMetrics.contentLength = this._selectLength({
@@ -1409,7 +1296,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
height,
});
const scrollMetrics = this._convertParentScrollMetrics(
this.context.virtualizedList.getScrollMetrics(),
this.context.getScrollMetrics(),
);
this._scrollMetrics.visibleLength = scrollMetrics.visibleLength;
this._scrollMetrics.offset = scrollMetrics.offset;
@@ -1988,12 +1875,6 @@ class CellRenderer extends React.Component<
},
};
static childContextTypes = {
virtualizedCell: PropTypes.shape({
cellKey: PropTypes.string,
}),
};
static getDerivedStateFromProps(
props: CellRendererProps,
prevState: CellRendererState,
@@ -2006,14 +1887,6 @@ class CellRenderer extends React.Component<
};
}
getChildContext() {
return {
virtualizedCell: {
cellKey: this.props.cellKey,
},
};
}
// TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not
// reused by SectionList and we can keep VirtualizedList simpler.
_separators = {
@@ -2119,18 +1992,15 @@ class CellRenderer extends React.Component<
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;
if (!CellRendererComponent) {
return (
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete
* this comment and run Flow. */
<View style={cellStyle} onLayout={onLayout}>
{element}
{itemSeparator}
</View>
);
}
return (
const result = !CellRendererComponent ? (
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete
* this comment and run Flow. */
<View style={cellStyle} onLayout={onLayout}>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
{...this.props}
style={cellStyle}
@@ -2139,30 +2009,12 @@ class CellRenderer extends React.Component<
{itemSeparator}
</CellRendererComponent>
);
}
}
class VirtualizedCellWrapper extends React.Component<{
cellKey: string,
children: React.Node,
...
}> {
static childContextTypes = {
virtualizedCell: PropTypes.shape({
cellKey: PropTypes.string,
}),
};
getChildContext() {
return {
virtualizedCell: {
cellKey: this.props.cellKey,
},
};
}
render() {
return this.props.children;
return (
<VirtualizedListCellContextProvider cellKey={this.props.cellKey}>
{result}
</VirtualizedListCellContextProvider>
);
}
}
+152
View File
@@ -0,0 +1,152 @@
/**
* 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
*/
'use strict';
import type VirtualizedList from './VirtualizedList.js';
import * as React from 'react';
import {useMemo, useContext} from 'react';
type Frame = $ReadOnly<{
offset: number,
length: number,
index: number,
inLayout: boolean,
}>;
export type ChildListState = $ReadOnly<{
first: number,
last: number,
frames: {[key: number]: Frame},
}>;
// 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: () => {
contentLength: number,
dOffset: number,
dt: number,
offset: number,
timestamp: number,
velocity: number,
visibleLength: number,
},
horizontal: ?boolean,
getOutermostParentListRef: () => VirtualizedList,
getNestedChildState: string => ?ChildListState,
registerAsNestedChild: ({
cellKey: string,
key: string,
ref: VirtualizedList,
parentDebugInfo: ListDebugInfo,
}) => ?ChildListState,
unregisterAsNestedChild: ({
key: string,
state: ChildListState,
}) => void,
debugInfo: ListDebugInfo,
}>;
export const VirtualizedListContext: React.Context<?Context> = React.createContext(
null,
);
/**
* Resets the context. Intended for use by portal-like components (e.g. Modal).
*/
export function VirtualizedListContextResetter({
children,
}: {
children: React.Node,
}): React.Node {
return (
<VirtualizedListContext.Provider value={null}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the context with memoization. Intended to be used by `VirtualizedList`.
*/
export function VirtualizedListContextProvider({
children,
value,
}: {
children: React.Node,
value: Context,
}): React.Node {
// Avoid setting a newly created context object if the values are identical.
const context = useMemo(
() => ({
cellKey: null,
getScrollMetrics: value.getScrollMetrics,
horizontal: value.horizontal,
getOutermostParentListRef: value.getOutermostParentListRef,
getNestedChildState: value.getNestedChildState,
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,
value.horizontal,
value.getOutermostParentListRef,
value.getNestedChildState,
value.registerAsNestedChild,
value.unregisterAsNestedChild,
value.debugInfo.cellKey,
value.debugInfo.horizontal,
value.debugInfo.listKey,
value.debugInfo.parent,
],
);
return (
<VirtualizedListContext.Provider value={context}>
{children}
</VirtualizedListContext.Provider>
);
}
/**
* Sets the `cellKey`. Intended to be used by `VirtualizedList` for each cell.
*/
export function VirtualizedListCellContextProvider({
cellKey,
children,
}: {
cellKey: string,
children: React.Node,
}): React.Node {
const context = useContext(VirtualizedListContext);
return (
<VirtualizedListContext.Provider
value={context == null ? null : {...context, cellKey}}>
{children}
</VirtualizedListContext.Provider>
);
}
+11 -19
View File
@@ -22,6 +22,8 @@ import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {DirectEventHandler} from '../Types/CodegenTypes';
import type EmitterSubscription from '../vendor/emitter/EmitterSubscription';
import RCTModalHostView from './RCTModalHostViewNativeComponent';
import {VirtualizedListContextResetter} from '../Lists/VirtualizedListContext.js';
/**
* The Modal component is a simple way to present content above an enclosing view.
*
@@ -159,20 +161,6 @@ class Modal extends React.Component<Props> {
this._identifier = uniqueModalIdentifier++;
}
static childContextTypes:
| any
| {|virtualizedList: React$PropType$Primitive<any>|} = {
virtualizedList: PropTypes.object,
};
getChildContext(): {|virtualizedList: null|} {
// Reset the context so VirtualizedList doesn't get confused by nesting
// in the React tree that doesn't reflect the native component hierarchy.
return {
virtualizedList: null,
};
}
componentWillUnmount() {
if (this.props.onDismiss != null) {
this.props.onDismiss();
@@ -236,11 +224,15 @@ class Modal extends React.Component<Props> {
onStartShouldSetResponder={this._shouldSetResponder}
supportedOrientations={this.props.supportedOrientations}
onOrientationChange={this.props.onOrientationChange}>
<ScrollView.Context.Provider value={null}>
<View style={[styles.container, containerStyles]} collapsable={false}>
{innerChildren}
</View>
</ScrollView.Context.Provider>
<VirtualizedListContextResetter>
<ScrollView.Context.Provider value={null}>
<View
style={[styles.container, containerStyles]}
collapsable={false}>
{innerChildren}
</View>
</ScrollView.Context.Provider>
</VirtualizedListContextResetter>
</RCTModalHostView>
);
}