Skip to content

Commit 8e3ce4c

Browse files
committed
New PreAsyncComponent warns about unsafe lifecycles in tree
Works like AsyncComponent except that it does not actually enable async rendering. This component is exposed via React.unstable_PreAsyncComponent (following precedent). I also tidied up ReactBaseClass a little because the duplication was bothering me. I will revert this if there's any concern. This branch is stacked on top of 12046-part-2 and PR #12060
1 parent cba51ba commit 8e3ce4c

6 files changed

+119
-36
lines changed

packages/react-reconciler/src/ReactDebugAsyncWarnings.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {Fiber} from './ReactFiber';
1111

1212
import getComponentName from 'shared/getComponentName';
1313
import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
14-
import {AsyncUpdates} from './ReactTypeOfInternalContext';
14+
import {AsyncUpdates, PreAsyncUpdates} from './ReactTypeOfInternalContext';
1515
import warning from 'fbjs/lib/warning';
1616

1717
type LIFECYCLE =
@@ -95,7 +95,10 @@ if (__DEV__) {
9595
let maybeAsyncRoot = null;
9696

9797
while (fiber !== null) {
98-
if (fiber.internalContextTag & AsyncUpdates) {
98+
if (
99+
fiber.internalContextTag & AsyncUpdates ||
100+
fiber.internalContextTag & PreAsyncUpdates
101+
) {
99102
maybeAsyncRoot = fiber;
100103
}
101104

packages/react-reconciler/src/ReactFiberClassComponent.js

+13-5
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import invariant from 'fbjs/lib/invariant';
2626
import warning from 'fbjs/lib/warning';
2727

2828
import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
29-
import {AsyncUpdates} from './ReactTypeOfInternalContext';
29+
import {AsyncUpdates, PreAsyncUpdates} from './ReactTypeOfInternalContext';
3030
import {
3131
cacheContext,
3232
getMaskedContext,
@@ -637,16 +637,24 @@ export default function(
637637
if (
638638
enableAsyncSubtreeAPI &&
639639
workInProgress.type != null &&
640-
workInProgress.type.prototype != null &&
641-
workInProgress.type.prototype.unstable_isAsyncReactComponent === true
640+
workInProgress.type.prototype != null
642641
) {
643-
workInProgress.internalContextTag |= AsyncUpdates;
642+
const prototype = workInProgress.type.prototype;
643+
644+
if (prototype.unstable_isAsyncReactComponent === true) {
645+
workInProgress.internalContextTag |= AsyncUpdates;
646+
} else if (prototype.unstable_isPreAsyncReactComponent === true) {
647+
workInProgress.internalContextTag |= PreAsyncUpdates;
648+
}
644649
}
645650

646651
if (__DEV__) {
647652
// If we're inside of an async sub-tree,
648653
// Warn about any unsafe lifecycles on this class component.
649-
if (workInProgress.internalContextTag & AsyncUpdates) {
654+
if (
655+
workInProgress.internalContextTag & AsyncUpdates ||
656+
workInProgress.internalContextTag & PreAsyncUpdates
657+
) {
650658
ReactDebugAsyncWarnings.recordLifecycleWarnings(
651659
workInProgress,
652660
instance,

packages/react-reconciler/src/ReactTypeOfInternalContext.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99

1010
export type TypeOfInternalContext = number;
1111

12-
export const NoContext = 0;
13-
export const AsyncUpdates = 1;
12+
export const NoContext = 0b00000000;
13+
export const AsyncUpdates = 0b00000001;
14+
export const PreAsyncUpdates = 0b00000010;

packages/react/src/React.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import assign from 'object-assign';
99
import ReactVersion from 'shared/ReactVersion';
1010
import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
1111

12-
import {Component, PureComponent, AsyncComponent} from './ReactBaseClasses';
12+
import {
13+
Component,
14+
PureComponent,
15+
AsyncComponent,
16+
PreAsyncComponent,
17+
} from './ReactBaseClasses';
1318
import {forEach, map, count, toArray, only} from './ReactChildren';
1419
import ReactCurrentOwner from './ReactCurrentOwner';
1520
import {
@@ -37,6 +42,7 @@ const React = {
3742
Component,
3843
PureComponent,
3944
unstable_AsyncComponent: AsyncComponent,
45+
unstable_PreAsyncComponent: PreAsyncComponent,
4046

4147
Fragment: REACT_FRAGMENT_TYPE,
4248

packages/react/src/ReactBaseClasses.js

+38-26
Original file line numberDiff line numberDiff line change
@@ -117,44 +117,56 @@ if (__DEV__) {
117117
}
118118
}
119119

120-
/**
121-
* Base class helpers for the updating state of a component.
122-
*/
120+
function ComponentDummy() {}
121+
ComponentDummy.prototype = Component.prototype;
122+
123+
function configurePrototype(ComponentSubclass, prototypeProperties) {
124+
const prototype = (ComponentSubclass.prototype = new ComponentDummy());
125+
prototype.constructor = ComponentSubclass;
126+
127+
// Avoid an extra prototype jump for these methods.
128+
Object.assign(prototype, Component.prototype);
129+
130+
// Mixin additional properties
131+
Object.assign(prototype, prototypeProperties);
132+
}
133+
134+
// Convenience component with default shallow equality check for sCU.
123135
function PureComponent(props, context, updater) {
124-
// Duplicated from Component.
125136
this.props = props;
126137
this.context = context;
127138
this.refs = emptyObject;
128-
// We initialize the default updater but the real one gets injected by the
129-
// renderer.
130139
this.updater = updater || ReactNoopUpdateQueue;
131140
}
141+
configurePrototype(PureComponent, {isPureReactComponent: true});
132142

133-
function ComponentDummy() {}
134-
ComponentDummy.prototype = Component.prototype;
135-
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
136-
pureComponentPrototype.constructor = PureComponent;
137-
// Avoid an extra prototype jump for these methods.
138-
Object.assign(pureComponentPrototype, Component.prototype);
139-
pureComponentPrototype.isPureReactComponent = true;
140-
143+
// Special component type that opts subtree into async rendering mode.
141144
function AsyncComponent(props, context, updater) {
142-
// Duplicated from Component.
143145
this.props = props;
144146
this.context = context;
145147
this.refs = emptyObject;
146-
// We initialize the default updater but the real one gets injected by the
147-
// renderer.
148148
this.updater = updater || ReactNoopUpdateQueue;
149149
}
150+
configurePrototype(AsyncComponent, {
151+
unstable_isAsyncReactComponent: true,
152+
render: function render() {
153+
return this.props.children;
154+
},
155+
});
150156

151-
const asyncComponentPrototype = (AsyncComponent.prototype = new ComponentDummy());
152-
asyncComponentPrototype.constructor = AsyncComponent;
153-
// Avoid an extra prototype jump for these methods.
154-
Object.assign(asyncComponentPrototype, Component.prototype);
155-
asyncComponentPrototype.unstable_isAsyncReactComponent = true;
156-
asyncComponentPrototype.render = function() {
157-
return this.props.children;
158-
};
157+
// Special component type that enables async rendering dev warnings.
158+
// This helps detect unsafe lifecycles without enabling actual async behavior.
159+
function PreAsyncComponent(props, context, updater) {
160+
this.props = props;
161+
this.context = context;
162+
this.refs = emptyObject;
163+
this.updater = updater || ReactNoopUpdateQueue;
164+
}
165+
configurePrototype(PreAsyncComponent, {
166+
unstable_isPreAsyncReactComponent: true,
167+
render: function render() {
168+
return this.props.children;
169+
},
170+
});
159171

160-
export {Component, PureComponent, AsyncComponent};
172+
export {Component, PureComponent, AsyncComponent, PreAsyncComponent};

packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js

+53
Original file line numberDiff line numberDiff line change
@@ -419,5 +419,58 @@ describe('ReactAsyncClassComponent', () => {
419419

420420
expect(caughtError).not.toBe(null);
421421
});
422+
423+
it('should also warn inside of pre-async trees', () => {
424+
class SyncRoot extends React.Component {
425+
UNSAFE_componentWillMount() {}
426+
UNSAFE_componentWillUpdate() {}
427+
UNSAFE_componentWillReceiveProps() {}
428+
render() {
429+
return <PreAsyncRoot />;
430+
}
431+
}
432+
class PreAsyncRoot extends React.unstable_PreAsyncComponent {
433+
UNSAFE_componentWillMount() {}
434+
render() {
435+
return <Wrapper />;
436+
}
437+
}
438+
function Wrapper({children}) {
439+
return (
440+
<div>
441+
<Bar />
442+
<Foo />
443+
</div>
444+
);
445+
}
446+
class Foo extends React.Component {
447+
UNSAFE_componentWillReceiveProps() {}
448+
render() {
449+
return null;
450+
}
451+
}
452+
class Bar extends React.Component {
453+
UNSAFE_componentWillReceiveProps() {}
454+
render() {
455+
return null;
456+
}
457+
}
458+
459+
expect(() => ReactTestRenderer.create(<SyncRoot />)).toWarnDev(
460+
'Unsafe lifecycle methods were found within the following async tree:' +
461+
'\n in PreAsyncRoot (at **)' +
462+
'\n in SyncRoot (at **)' +
463+
'\n\ncomponentWillMount: Please update the following components ' +
464+
'to use componentDidMount instead: PreAsyncRoot' +
465+
'\n\ncomponentWillReceiveProps: Please update the following components ' +
466+
'to use static getDerivedStateFromProps instead: Bar, Foo' +
467+
'\n\nLearn more about this warning here:' +
468+
'\nhttps://fb.me/react-async-component-lifecycle-hooks',
469+
);
470+
471+
// Dedupe
472+
const rendered = ReactTestRenderer.create(<SyncRoot />);
473+
rendered.update(<SyncRoot />);
474+
});
422475
});
423476
});

0 commit comments

Comments
 (0)