diff --git a/Libraries/Lists/StateSafePureComponent.js b/Libraries/Lists/StateSafePureComponent.js new file mode 100644 index 00000000000000..811cc02689011f --- /dev/null +++ b/Libraries/Lists/StateSafePureComponent.js @@ -0,0 +1,85 @@ +/** + * 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 * as React from 'react'; +import invariant from 'invariant'; + +/** + * `setState` is called asynchronously, and should not rely on the value of + * `this.props` or `this.state`: + * https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous + * + * SafePureComponent adds runtime enforcement, to catch cases where these + * variables are read in a state updater function, instead of the ones passed + * in. + */ +export default class StateSafePureComponent< + Props, + State: interface {}, +> extends React.PureComponent { + _inAsyncStateUpdate = false; + + constructor(props: Props) { + super(props); + this._installSetStateHooks(); + } + + setState( + partialState: ?($Shape | ((State, Props) => ?$Shape)), + callback?: () => mixed, + ): void { + if (typeof partialState === 'function') { + super.setState((state, props) => { + this._inAsyncStateUpdate = true; + let ret; + try { + ret = partialState(state, props); + } catch (err) { + throw err; + } finally { + this._inAsyncStateUpdate = false; + } + return ret; + }, callback); + } else { + super.setState(partialState, callback); + } + } + + _installSetStateHooks() { + const that = this; + let {props, state} = this; + + Object.defineProperty(this, 'props', { + get() { + invariant( + !that._inAsyncStateUpdate, + '"this.props" should not be accessed during state updates', + ); + return props; + }, + set(newProps: Props) { + props = newProps; + }, + }); + Object.defineProperty(this, 'state', { + get() { + invariant( + !that._inAsyncStateUpdate, + '"this.state" should not be acceessed during state updates', + ); + return state; + }, + set(newState: State) { + state = newState; + }, + }); + } +} diff --git a/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js b/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js index f3c834e09e36f4..1b1acb757d6644 100644 --- a/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js +++ b/Libraries/Lists/VirtualizedList_EXPERIMENTAL.js @@ -43,6 +43,7 @@ import * as React from 'react'; import {CellRenderMask} from './CellRenderMask'; import clamp from '../Utilities/clamp'; +import StateSafePureComponent from './StateSafePureComponent'; const RefreshControl = require('../Components/RefreshControl/RefreshControl'); const ScrollView = require('../Components/ScrollView/ScrollView'); @@ -159,7 +160,7 @@ function findLastWhere( * - As an effort to remove defaultProps, use helper functions when referencing certain props * */ -class VirtualizedList extends React.PureComponent { +class VirtualizedList extends StateSafePureComponent { static contextType: typeof VirtualizedListContext = VirtualizedListContext; // scrollToEnd may be janky without getItemLayout prop @@ -2252,4 +2253,5 @@ const styles = StyleSheet.create({ }, }); +VirtualizedList.displayName = 'VirtualizedList_EXPERIMENTAL'; module.exports = VirtualizedList;