From 7bd694fc6f4bb027b6d7ee04034cad41a43e5695 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Mon, 4 May 2020 00:57:01 -0700 Subject: [PATCH] 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 --- Libraries/Lists/VirtualizedList.js | 274 +++++----------------- Libraries/Lists/VirtualizedListContext.js | 152 ++++++++++++ Libraries/Modal/Modal.js | 30 +-- 3 files changed, 226 insertions(+), 230 deletions(-) create mode 100644 Libraries/Lists/VirtualizedListContext.js diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 5a7205b7652..bf65fbdd107 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -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: 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 { windowSize: 21, // multiples of length }; - static contextTypes: - | any - | {| - virtualizedCell: {| - cellKey: React$PropType$Primitive, - |}, - virtualizedList: {| - getScrollMetrics: React$PropType$Primitive, - horizontal: React$PropType$Primitive, - getOutermostParentListRef: React$PropType$Primitive, - getNestedChildState: React$PropType$Primitive, - registerAsNestedChild: React$PropType$Primitive, - unregisterAsNestedChild: React$PropType$Primitive, - debugInfo: {| - listKey: React$PropType$Primitive, - cellKey: React$PropType$Primitive, - |}, - |}, - |} = { - 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, - horizontal: React$PropType$Primitive, - getOutermostParentListRef: React$PropType$Primitive, - getNestedChildState: React$PropType$Primitive, - registerAsNestedChild: React$PropType$Primitive, - unregisterAsNestedChild: React$PropType$Primitive, - |} = { - 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 { 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 { _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 { 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 { }; 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 { 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 { // 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 { } _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 { ); cells.push( - { element } - , + , ); } const itemCount = this.props.getItemCount(data); @@ -1139,7 +1011,7 @@ class VirtualizedList extends React.PureComponent { ); cells.push( - { element } - , + , ); } const scrollProps = { @@ -1178,14 +1050,29 @@ class VirtualizedList extends React.PureComponent { 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 = ( + + {React.cloneElement( + ( + this.props.renderScrollComponent || + this._defaultRenderScrollComponent + )(scrollProps), + { + ref: this._captureScrollRef, + }, + cells, + )} + ); let ret = innerRet; if (__DEV__) { @@ -1196,7 +1083,7 @@ class VirtualizedList extends React.PureComponent { 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 { // 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 { 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. */ - - {element} - {itemSeparator} - - ); - } - 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. */ + + {element} + {itemSeparator} + + ) : ( ); - } -} -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 ( + + {result} + + ); } } diff --git a/Libraries/Lists/VirtualizedListContext.js b/Libraries/Lists/VirtualizedListContext.js new file mode 100644 index 00000000000..130315a1b08 --- /dev/null +++ b/Libraries/Lists/VirtualizedListContext.js @@ -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 = 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 ( + + {children} + + ); +} + +/** + * 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 ( + + {children} + + ); +} + +/** + * 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 ( + + {children} + + ); +} diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 17885714dd0..b81334c91db 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -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 { this._identifier = uniqueModalIdentifier++; } - static childContextTypes: - | any - | {|virtualizedList: React$PropType$Primitive|} = { - 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 { onStartShouldSetResponder={this._shouldSetResponder} supportedOrientations={this.props.supportedOrientations} onOrientationChange={this.props.onOrientationChange}> - - - {innerChildren} - - + + + + {innerChildren} + + + ); }