diff --git a/packages/react-reconciler/src/ReactDebugAsyncWarnings.js b/packages/react-reconciler/src/ReactDebugAsyncWarnings.js index 5aeb102d69718..0dbd802df95af 100644 --- a/packages/react-reconciler/src/ReactDebugAsyncWarnings.js +++ b/packages/react-reconciler/src/ReactDebugAsyncWarnings.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactFiber'; import getComponentName from 'shared/getComponentName'; import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; -import {AsyncUpdates} from './ReactTypeOfInternalContext'; +import {AsyncUpdates, PreAsyncUpdates} from './ReactTypeOfInternalContext'; import warning from 'fbjs/lib/warning'; type LIFECYCLE = @@ -94,7 +94,10 @@ if (__DEV__) { let maybeAsyncRoot = null; while (fiber !== null) { - if (fiber.internalContextTag & AsyncUpdates) { + if ( + fiber.internalContextTag & AsyncUpdates || + fiber.internalContextTag & PreAsyncUpdates + ) { maybeAsyncRoot = fiber; } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 4eed41271806d..9a9bb5f978a66 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -26,7 +26,7 @@ import invariant from 'fbjs/lib/invariant'; import warning from 'fbjs/lib/warning'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; -import {AsyncUpdates} from './ReactTypeOfInternalContext'; +import {AsyncUpdates, PreAsyncUpdates} from './ReactTypeOfInternalContext'; import { cacheContext, getMaskedContext, @@ -637,16 +637,24 @@ export default function( if ( enableAsyncSubtreeAPI && workInProgress.type != null && - workInProgress.type.prototype != null && - workInProgress.type.prototype.unstable_isAsyncReactComponent === true + workInProgress.type.prototype != null ) { - workInProgress.internalContextTag |= AsyncUpdates; + const prototype = workInProgress.type.prototype; + + if (prototype.unstable_isAsyncReactComponent === true) { + workInProgress.internalContextTag |= AsyncUpdates; + } else if (prototype.unstable_isPreAsyncReactComponent === true) { + workInProgress.internalContextTag |= PreAsyncUpdates; + } } if (__DEV__) { // If we're inside of an async sub-tree, // Warn about any unsafe lifecycles on this class component. - if (workInProgress.internalContextTag & AsyncUpdates) { + if ( + workInProgress.internalContextTag & AsyncUpdates || + workInProgress.internalContextTag & PreAsyncUpdates + ) { ReactDebugAsyncWarnings.recordLifecycleWarnings( workInProgress, instance, diff --git a/packages/react-reconciler/src/ReactTypeOfInternalContext.js b/packages/react-reconciler/src/ReactTypeOfInternalContext.js index 24859845f79f0..015c90cca2d63 100644 --- a/packages/react-reconciler/src/ReactTypeOfInternalContext.js +++ b/packages/react-reconciler/src/ReactTypeOfInternalContext.js @@ -9,5 +9,6 @@ export type TypeOfInternalContext = number; -export const NoContext = 0; -export const AsyncUpdates = 1; +export const NoContext = 0b00000000; +export const AsyncUpdates = 0b00000001; +export const PreAsyncUpdates = 0b00000010; diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 61a59f83c530b..4a2081edbe86c 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -9,7 +9,12 @@ import assign from 'object-assign'; import ReactVersion from 'shared/ReactVersion'; import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols'; -import {Component, PureComponent, AsyncComponent} from './ReactBaseClasses'; +import { + Component, + PureComponent, + AsyncComponent, + PreAsyncComponent, +} from './ReactBaseClasses'; import {forEach, map, count, toArray, only} from './ReactChildren'; import ReactCurrentOwner from './ReactCurrentOwner'; import { @@ -37,6 +42,7 @@ const React = { Component, PureComponent, unstable_AsyncComponent: AsyncComponent, + unstable_PreAsyncComponent: PreAsyncComponent, Fragment: REACT_FRAGMENT_TYPE, diff --git a/packages/react/src/ReactBaseClasses.js b/packages/react/src/ReactBaseClasses.js index 13d4ab999bfba..b95d158041198 100644 --- a/packages/react/src/ReactBaseClasses.js +++ b/packages/react/src/ReactBaseClasses.js @@ -117,44 +117,56 @@ if (__DEV__) { } } -/** - * Base class helpers for the updating state of a component. - */ +function ComponentDummy() {} +ComponentDummy.prototype = Component.prototype; + +function configurePrototype(ComponentSubclass, prototypeProperties) { + const prototype = (ComponentSubclass.prototype = new ComponentDummy()); + prototype.constructor = ComponentSubclass; + + // Avoid an extra prototype jump for these methods. + Object.assign(prototype, Component.prototype); + + // Mixin additional properties + Object.assign(prototype, prototypeProperties); +} + +// Convenience component with default shallow equality check for sCU. function PureComponent(props, context, updater) { - // Duplicated from Component. this.props = props; this.context = context; this.refs = emptyObject; - // We initialize the default updater but the real one gets injected by the - // renderer. this.updater = updater || ReactNoopUpdateQueue; } +configurePrototype(PureComponent, {isPureReactComponent: true}); -function ComponentDummy() {} -ComponentDummy.prototype = Component.prototype; -const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); -pureComponentPrototype.constructor = PureComponent; -// Avoid an extra prototype jump for these methods. -Object.assign(pureComponentPrototype, Component.prototype); -pureComponentPrototype.isPureReactComponent = true; - +// Special component type that opts subtree into async rendering mode. function AsyncComponent(props, context, updater) { - // Duplicated from Component. this.props = props; this.context = context; this.refs = emptyObject; - // We initialize the default updater but the real one gets injected by the - // renderer. this.updater = updater || ReactNoopUpdateQueue; } +configurePrototype(AsyncComponent, { + unstable_isAsyncReactComponent: true, + render: function render() { + return this.props.children; + }, +}); -const asyncComponentPrototype = (AsyncComponent.prototype = new ComponentDummy()); -asyncComponentPrototype.constructor = AsyncComponent; -// Avoid an extra prototype jump for these methods. -Object.assign(asyncComponentPrototype, Component.prototype); -asyncComponentPrototype.unstable_isAsyncReactComponent = true; -asyncComponentPrototype.render = function() { - return this.props.children; -}; +// Special component type that enables async rendering dev warnings. +// This helps detect unsafe lifecycles without enabling actual async behavior. +function PreAsyncComponent(props, context, updater) { + this.props = props; + this.context = context; + this.refs = emptyObject; + this.updater = updater || ReactNoopUpdateQueue; +} +configurePrototype(PreAsyncComponent, { + unstable_isPreAsyncReactComponent: true, + render: function render() { + return this.props.children; + }, +}); -export {Component, PureComponent, AsyncComponent}; +export {Component, PureComponent, AsyncComponent, PreAsyncComponent}; diff --git a/packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js b/packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js index a559d401001ea..e8e6a51538133 100644 --- a/packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js +++ b/packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js @@ -419,5 +419,58 @@ describe('ReactAsyncClassComponent', () => { expect(caughtError).not.toBe(null); }); + + it('should also warn inside of pre-async trees', () => { + class SyncRoot extends React.Component { + UNSAFE_componentWillMount() {} + UNSAFE_componentWillUpdate() {} + UNSAFE_componentWillReceiveProps() {} + render() { + return ; + } + } + class PreAsyncRoot extends React.unstable_PreAsyncComponent { + UNSAFE_componentWillMount() {} + render() { + return ; + } + } + function Wrapper({children}) { + return ( +
+ + +
+ ); + } + class Foo extends React.Component { + UNSAFE_componentWillReceiveProps() {} + render() { + return null; + } + } + class Bar extends React.Component { + UNSAFE_componentWillReceiveProps() {} + render() { + return null; + } + } + + expect(() => ReactTestRenderer.create()).toWarnDev( + 'Unsafe lifecycle methods were found within the following async tree:' + + '\n in PreAsyncRoot (at **)' + + '\n in SyncRoot (at **)' + + '\n\ncomponentWillMount: Please update the following components ' + + 'to use componentDidMount instead: PreAsyncRoot' + + '\n\ncomponentWillReceiveProps: Please update the following components ' + + 'to use static getDerivedStateFromProps instead: Bar, Foo' + + '\n\nLearn more about this warning here:' + + '\nhttps://fb.me/react-async-component-lifecycle-hooks', + ); + + // Dedupe + const rendered = ReactTestRenderer.create(); + rendered.update(); + }); }); });