@samitouri / QOS-React-2 / commits / cba51badce

Warn if unsafe lifecycle methods are found in an async subtree (#12060)

Brian Vaughn committed Jan 23, 2018 at 14:01 UTC cba51badce7923f76770149a3248a7baab87bc92
10 files changed +529 -12
packages/react-call-return/src/__tests__/ReactCallReturn-test.js
+4 -1
@@ -290,7 +290,10 @@ describe('ReactCallReturn', () => {
290 <Return value={2} />
291 </Call>,
292 );
293 - ReactNoop.flush();
293 + expect(ReactNoop.flush).toWarnDev(
294 + 'componentWillMount: Please update the following components ' +
295 + 'to use componentDidMount instead: Return',
296 + );
297
298 expect(ops).toEqual([
299 'Mount Return 1',
packages/react-reconciler/src/ReactDebugAsyncWarnings.js new
+166
@@ -0,0 +1,166 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Fiber} from './ReactFiber';
11 +
12 +import getComponentName from 'shared/getComponentName';
13 +import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
14 +import {AsyncUpdates} from './ReactTypeOfInternalContext';
15 +import warning from 'fbjs/lib/warning';
16 +
17 +type LIFECYCLE =
18 + | 'UNSAFE_componentWillMount'
19 + | 'UNSAFE_componentWillReceiveProps'
20 + | 'UNSAFE_componentWillUpdate';
21 +type LifecycleToComponentsMap = {[lifecycle: LIFECYCLE]: Array<Fiber>};
22 +type FiberToLifecycleMap = Map<Fiber, LifecycleToComponentsMap>;
23 +
24 +const ReactDebugAsyncWarnings = {
25 + discardPendingWarnings(): void {},
26 + flushPendingAsyncWarnings(): void {},
27 + recordLifecycleWarnings(fiber: Fiber, instance: any): void {},
28 +};
29 +
30 +if (__DEV__) {
31 + const LIFECYCLE_SUGGESTIONS = {
32 + UNSAFE_componentWillMount: 'componentDidMount',
33 + UNSAFE_componentWillReceiveProps: 'static getDerivedStateFromProps',
34 + UNSAFE_componentWillUpdate: 'componentDidUpdate',
35 + };
36 +
37 + let pendingWarningsMap: FiberToLifecycleMap = new Map();
38 +
39 + // Tracks components we have already warned about.
40 + const didWarnSet = new Set();
41 +
42 + ReactDebugAsyncWarnings.discardPendingWarnings = () => {
43 + pendingWarningsMap = new Map();
44 + };
45 +
46 + ReactDebugAsyncWarnings.flushPendingAsyncWarnings = () => {
47 + ((pendingWarningsMap: any): FiberToLifecycleMap).forEach(
48 + (lifecycleWarningsMap, asyncRoot) => {
49 + const lifecyclesWarningMesages = [];
50 +
51 + Object.keys(lifecycleWarningsMap).forEach(lifecycle => {
52 + const lifecycleWarnings = lifecycleWarningsMap[lifecycle];
53 + if (lifecycleWarnings.length > 0) {
54 + const componentNames = new Set();
55 + lifecycleWarnings.forEach(fiber => {
56 + componentNames.add(getComponentName(fiber) || 'Component');
57 + didWarnSet.add(fiber.type);
58 + });
59 +
60 + const formatted = lifecycle.replace('UNSAFE_', '');
61 + const suggestion = LIFECYCLE_SUGGESTIONS[lifecycle];
62 + const sortedComponentNames = Array.from(componentNames)
63 + .sort()
64 + .join(', ');
65 +
66 + lifecyclesWarningMesages.push(
67 + `${formatted}: Please update the following components to use ` +
68 + `${suggestion} instead: ${sortedComponentNames}`,
69 + );
70 + }
71 + });
72 +
73 + if (lifecyclesWarningMesages.length > 0) {
74 + const asyncRootComponentStack = getStackAddendumByWorkInProgressFiber(
75 + asyncRoot,
76 + );
77 +
78 + warning(
79 + false,
80 + 'Unsafe lifecycle methods were found within the following async tree:%s' +
81 + '\n\n%s' +
82 + '\n\nLearn more about this warning here:' +
83 + '\nhttps://fb.me/react-async-component-lifecycle-hooks',
84 + asyncRootComponentStack,
85 + lifecyclesWarningMesages.join('\n\n'),
86 + );
87 + }
88 + },
89 + );
90 +
91 + pendingWarningsMap = new Map();
92 + };
93 +
94 + const getAsyncRoot = (fiber: Fiber): Fiber => {
95 + let maybeAsyncRoot = null;
96 +
97 + while (fiber !== null) {
98 + if (fiber.internalContextTag & AsyncUpdates) {
99 + maybeAsyncRoot = fiber;
100 + }
101 +
102 + fiber = fiber.return;
103 + }
104 +
105 + return maybeAsyncRoot;
106 + };
107 +
108 + ReactDebugAsyncWarnings.recordLifecycleWarnings = (
109 + fiber: Fiber,
110 + instance: any,
111 + ) => {
112 + const asyncRoot = getAsyncRoot(fiber);
113 +
114 + // Dedup strategy: Warn once per component.
115 + // This is difficult to track any other way since component names
116 + // are often vague and are likely to collide between 3rd party libraries.
117 + // An expand property is probably okay to use here since it's DEV-only,
118 + // and will only be set in the event of serious warnings.
119 + if (didWarnSet.has(fiber.type)) {
120 + return;
121 + }
122 +
123 + let warningsForRoot;
124 + if (!pendingWarningsMap.has(asyncRoot)) {
125 + warningsForRoot = {
126 + UNSAFE_componentWillMount: [],
127 + UNSAFE_componentWillReceiveProps: [],
128 + UNSAFE_componentWillUpdate: [],
129 + };
130 +
131 + pendingWarningsMap.set(asyncRoot, warningsForRoot);
132 + } else {
133 + warningsForRoot = pendingWarningsMap.get(asyncRoot);
134 + }
135 +
136 + const unsafeLifecycles = [];
137 + if (
138 + typeof instance.componentWillMount === 'function' ||
139 + typeof instance.UNSAFE_componentWillMount === 'function'
140 + ) {
141 + unsafeLifecycles.push('UNSAFE_componentWillMount');
142 + }
143 + if (
144 + typeof instance.componentWillReceiveProps === 'function' ||
145 + typeof instance.UNSAFE_componentWillReceiveProps === 'function'
146 + ) {
147 + unsafeLifecycles.push('UNSAFE_componentWillReceiveProps');
148 + }
149 + if (
150 + typeof instance.componentWillUpdate === 'function' ||
151 + typeof instance.UNSAFE_componentWillUpdate === 'function'
152 + ) {
153 + unsafeLifecycles.push('UNSAFE_componentWillUpdate');
154 + }
155 +
156 + if (unsafeLifecycles.length > 0) {
157 + unsafeLifecycles.forEach(lifecycle => {
158 + ((warningsForRoot: any): LifecycleToComponentsMap)[lifecycle].push(
159 + fiber,
160 + );
161 + });
162 + }
163 + };
164 +}
165 +
166 +export default ReactDebugAsyncWarnings;
packages/react-reconciler/src/ReactFiberClassComponent.js
+12
@@ -16,6 +16,7 @@ import {
16 enableAsyncSubtreeAPI,
17 warnAboutDeprecatedLifecycles,
18 } from 'shared/ReactFeatureFlags';
19 +import ReactDebugAsyncWarnings from './ReactDebugAsyncWarnings';
20 import {isMounted} from 'react-reconciler/reflection';
21 import * as ReactInstanceMap from 'shared/ReactInstanceMap';
22 import emptyObject from 'fbjs/lib/emptyObject';
@@ -642,6 +643,17 @@ export default function(
643 workInProgress.internalContextTag |= AsyncUpdates;
644 }
645
646 + if (__DEV__) {
647 + // If we're inside of an async sub-tree,
648 + // Warn about any unsafe lifecycles on this class component.
649 + if (workInProgress.internalContextTag & AsyncUpdates) {
650 + ReactDebugAsyncWarnings.recordLifecycleWarnings(
651 + workInProgress,
652 + instance,
653 + );
654 + }
655 + }
656 +
657 if (
658 typeof instance.UNSAFE_componentWillMount === 'function' ||
659 typeof instance.componentWillMount === 'function'
packages/react-reconciler/src/ReactFiberScheduler.js
+9
@@ -16,6 +16,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
16 import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook';
17 import ReactErrorUtils from 'shared/ReactErrorUtils';
18 import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
19 +import ReactDebugAsyncWarnings from './ReactDebugAsyncWarnings';
20 import {
21 PerformedWork,
22 Placement,
@@ -310,6 +311,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
311 }
312
313 function commitAllLifeCycles() {
314 + if (__DEV__) {
315 + ReactDebugAsyncWarnings.flushPendingAsyncWarnings();
316 + }
317 +
318 while (nextEffect !== null) {
319 const effectTag = nextEffect.effectTag;
320
@@ -651,6 +656,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
656 }
657
658 function performFailedUnitOfWork(workInProgress: Fiber): Fiber | null {
659 + if (__DEV__) {
660 + ReactDebugAsyncWarnings.discardPendingWarnings();
661 + }
662 +
663 // The current, flushed, state of this fiber is the alternate.
664 // Ideally nothing should rely on this, but relying on it here
665 // means that we don't need an additional field on the work in
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+7 -4
@@ -1430,8 +1430,8 @@ describe('ReactIncremental', () => {
1430 changeState() {
1431 this.setState({foo: 'bar'});
1432 }
1433 - componentWillUpdate() {
1434 - ops.push('componentWillUpdate');
1433 + componentDidUpdate() {
1434 + ops.push('componentDidUpdate');
1435 }
1436 render() {
1437 ops.push('render');
@@ -1451,7 +1451,7 @@ describe('ReactIncremental', () => {
1451 instance.changeState();
1452 ReactNoop.flush();
1453
1454 - expect(ops).toEqual(['componentWillUpdate', 'render']);
1454 + expect(ops).toEqual(['render', 'componentDidUpdate']);
1455 expect(instance.state).toEqual({foo: 'bar'});
1456 });
1457
@@ -2335,7 +2335,10 @@ describe('ReactIncremental', () => {
2335 }
2336
2337 ReactNoop.render(<MyComponent />);
2338 - ReactNoop.flush();
2338 + expect(ReactNoop.flush).toWarnDev(
2339 + 'componentWillReceiveProps: Please update the following components ' +
2340 + 'to use static getDerivedStateFromProps instead: MyComponent',
2341 + );
2342
2343 expect(ops).toEqual([
2344 'render',
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js
+14 -2
@@ -254,7 +254,12 @@ describe('ReactDebugFiberPerf', () => {
254 </Parent>,
255 );
256 addComment('Should not print a warning');
257 - ReactNoop.flush();
257 + expect(ReactNoop.flush).toWarnDev([
258 + 'componentWillMount: Please update the following components ' +
259 + 'to use componentDidMount instead: NotCascading' +
260 + '\n\ncomponentWillReceiveProps: Please update the following components ' +
261 + 'to use static getDerivedStateFromProps instead: NotCascading',
262 + ]);
263 ReactNoop.render(
264 <Parent>
265 <NotCascading />
@@ -288,7 +293,14 @@ describe('ReactDebugFiberPerf', () => {
293 }
294 ReactNoop.render(<AllLifecycles />);
295 addComment('Mount');
291 - ReactNoop.flush();
296 + expect(ReactNoop.flush).toWarnDev(
297 + 'componentWillMount: Please update the following components ' +
298 + 'to use componentDidMount instead: AllLifecycles' +
299 + '\n\ncomponentWillReceiveProps: Please update the following components ' +
300 + 'to use static getDerivedStateFromProps instead: AllLifecycles' +
301 + '\n\ncomponentWillUpdate: Please update the following components ' +
302 + 'to use componentDidUpdate instead: AllLifecycles',
303 + );
304 ReactNoop.render(<AllLifecycles />);
305 addComment('Update');
306 ReactNoop.flush();
packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.js
+14 -3
@@ -59,7 +59,10 @@ describe('ReactIncrementalReflection', () => {
59 ops = [];
60
61 // Render the rest and commit the updates.
62 - ReactNoop.flush();
62 + expect(ReactNoop.flush).toWarnDev(
63 + 'componentWillMount: Please update the following components ' +
64 + 'to use componentDidMount instead: Component',
65 + );
66
67 expect(ops).toEqual(['componentDidMount', true]);
68
@@ -97,7 +100,10 @@ describe('ReactIncrementalReflection', () => {
100 }
101
102 ReactNoop.render(<Foo mount={true} />);
100 - ReactNoop.flush();
103 + expect(ReactNoop.flush).toWarnDev(
104 + 'componentWillMount: Please update the following components ' +
105 + 'to use componentDidMount instead: Component',
106 + );
107
108 expect(ops).toEqual(['Component']);
109 ops = [];
@@ -184,7 +190,12 @@ describe('ReactIncrementalReflection', () => {
190 // not find any host nodes in it.
191 expect(ReactNoop.findInstance(classInstance)).toBe(null);
192
187 - ReactNoop.flush();
193 + expect(ReactNoop.flush).toWarnDev(
194 + 'componentWillMount: Please update the following components ' +
195 + 'to use componentDidMount instead: Component' +
196 + '\n\ncomponentWillUpdate: Please update the following components ' +
197 + 'to use componentDidUpdate instead: Component',
198 + );
199
200 const hostSpan = classInstance.span;
201 expect(hostSpan).toBeDefined();
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.js
+4 -1
@@ -346,7 +346,10 @@ describe('ReactIncrementalScheduling', () => {
346 }
347
348 ReactNoop.render(<Foo step={1} />);
349 - ReactNoop.flush();
349 + expect(ReactNoop.flush).toWarnDev(
350 + 'componentWillReceiveProps: Please update the following components ' +
351 + 'to use static getDerivedStateFromProps instead: Foo',
352 + );
353
354 ReactNoop.render(<Foo step={2} />);
355 expect(ReactNoop.flush()).toEqual([
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+5 -1
@@ -307,12 +307,16 @@ describe('ReactIncrementalUpdates', () => {
307 }
308 }
309 ReactNoop.render(<Foo />);
310 - ReactNoop.flush();
310 + expect(ReactNoop.flush).toWarnDev(
311 + 'componentWillReceiveProps: Please update the following components ' +
312 + 'to use static getDerivedStateFromProps instead: Foo',
313 + );
314
315 ops = [];
316
317 ReactNoop.flushSync(() => {
318 instance.setState({a: 'a'});
319 +
320 ReactNoop.render(<Foo />); // Trigger componentWillReceiveProps
321 });
322
packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js
+294
@@ -126,4 +126,298 @@ describe('ReactAsyncClassComponent', () => {
126 expect(instance.state.count).toBe(2);
127 });
128 });
129 +
130 + describe('async subtree', () => {
131 + beforeEach(() => {
132 + jest.resetModules();
133 +
134 + React = require('react');
135 + ReactTestRenderer = require('react-test-renderer');
136 + });
137 +
138 + it('should warn about unsafe legacy lifecycle methods within the tree', () => {
139 + class SyncRoot extends React.Component {
140 + UNSAFE_componentWillMount() {}
141 + UNSAFE_componentWillUpdate() {}
142 + UNSAFE_componentWillReceiveProps() {}
143 + render() {
144 + return <AsyncRoot />;
145 + }
146 + }
147 + class AsyncRoot extends React.unstable_AsyncComponent {
148 + UNSAFE_componentWillMount() {}
149 + UNSAFE_componentWillUpdate() {}
150 + render() {
151 + return (
152 + <div>
153 + <Wrapper>
154 + <Foo />
155 + </Wrapper>
156 + <div>
157 + <Bar />
158 + <Foo />
159 + </div>
160 + </div>
161 + );
162 + }
163 + }
164 + function Wrapper({children}) {
165 + return <div>{children}</div>;
166 + }
167 + class Foo extends React.Component {
168 + UNSAFE_componentWillReceiveProps() {}
169 + render() {
170 + return null;
171 + }
172 + }
173 + class Bar extends React.Component {
174 + UNSAFE_componentWillReceiveProps() {}
175 + render() {
176 + return null;
177 + }
178 + }
179 +
180 + let rendered;
181 + expect(() => {
182 + rendered = ReactTestRenderer.create(<SyncRoot />);
183 + }).toWarnDev(
184 + 'Unsafe lifecycle methods were found within the following async tree:' +
185 + '\n in AsyncRoot (at **)' +
186 + '\n in SyncRoot (at **)' +
187 + '\n\ncomponentWillMount: Please update the following components ' +
188 + 'to use componentDidMount instead: AsyncRoot' +
189 + '\n\ncomponentWillReceiveProps: Please update the following components ' +
190 + 'to use static getDerivedStateFromProps instead: Bar, Foo' +
191 + '\n\ncomponentWillUpdate: Please update the following components ' +
192 + 'to use componentDidUpdate instead: AsyncRoot' +
193 + '\n\nLearn more about this warning here:' +
194 + '\nhttps://fb.me/react-async-component-lifecycle-hooks',
195 + );
196 +
197 + // Dedupe
198 + rendered = ReactTestRenderer.create(<SyncRoot />);
199 + rendered.update(<SyncRoot />);
200 + });
201 +
202 + it('should coalesce warnings by lifecycle name', () => {
203 + class SyncRoot extends React.Component {
204 + UNSAFE_componentWillMount() {}
205 + UNSAFE_componentWillUpdate() {}
206 + UNSAFE_componentWillReceiveProps() {}
207 + render() {
208 + return <AsyncRoot />;
209 + }
210 + }
211 + class AsyncRoot extends React.unstable_AsyncComponent {
212 + UNSAFE_componentWillMount() {}
213 + UNSAFE_componentWillUpdate() {}
214 + render() {
215 + return <Parent />;
216 + }
217 + }
218 + class Parent extends React.Component {
219 + componentWillMount() {}
220 + componentWillUpdate() {}
221 + componentWillReceiveProps() {}
222 + render() {
223 + return <Child />;
224 + }
225 + }
226 + class Child extends React.Component {
227 + UNSAFE_componentWillReceiveProps() {}
228 + render() {
229 + return null;
230 + }
231 + }
232 +
233 + let rendered;
234 +
235 + expect(
236 + () => (rendered = ReactTestRenderer.create(<SyncRoot />)),
237 + ).toWarnDev(
238 + 'Unsafe lifecycle methods were found within the following async tree:' +
239 + '\n in AsyncRoot (at **)' +
240 + '\n in SyncRoot (at **)' +
241 + '\n\ncomponentWillMount: Please update the following components ' +
242 + 'to use componentDidMount instead: AsyncRoot, Parent' +
243 + '\n\ncomponentWillReceiveProps: Please update the following components ' +
244 + 'to use static getDerivedStateFromProps instead: Child, Parent' +
245 + '\n\ncomponentWillUpdate: Please update the following components ' +
246 + 'to use componentDidUpdate instead: AsyncRoot, Parent' +
247 + '\n\nLearn more about this warning here:' +
248 + '\nhttps://fb.me/react-async-component-lifecycle-hooks',
249 + );
250 +
251 + // Dedupe
252 + rendered = ReactTestRenderer.create(<SyncRoot />);
253 + rendered.update(<SyncRoot />);
254 + });
255 +
256 + it('should group warnings by async root', () => {
257 + class SyncRoot extends React.Component {
258 + UNSAFE_componentWillMount() {}
259 + UNSAFE_componentWillUpdate() {}
260 + UNSAFE_componentWillReceiveProps() {}
261 + render() {
262 + return (
263 + <div>
264 + <AsyncRootOne />
265 + <AsyncRootTwo />
266 + </div>
267 + );
268 + }
269 + }
270 + class AsyncRootOne extends React.unstable_AsyncComponent {
271 + render() {
272 + return (
273 + <Foo>
274 + <Bar />
275 + </Foo>
276 + );
277 + }
278 + }
279 + class AsyncRootTwo extends React.unstable_AsyncComponent {
280 + render() {
281 + return (
282 + <Foo>
283 + <Baz />
284 + </Foo>
285 + );
286 + }
287 + }
288 + class Foo extends React.Component {
289 + componentWillMount() {}
290 + render() {
291 + return this.props.children;
292 + }
293 + }
294 + class Bar extends React.Component {
295 + componentWillMount() {}
296 + render() {
297 + return null;
298 + }
299 + }
300 + class Baz extends React.Component {
301 + componentWillMount() {}
302 + render() {
303 + return null;
304 + }
305 + }
306 +
307 + let rendered;
308 +
309 + expect(
310 + () => (rendered = ReactTestRenderer.create(<SyncRoot />)),
311 + ).toWarnDev([
312 + 'Unsafe lifecycle methods were found within the following async tree:' +
313 + '\n in AsyncRootOne (at **)' +
314 + '\n in div (at **)' +
315 + '\n in SyncRoot (at **)' +
316 + '\n\ncomponentWillMount: Please update the following components ' +
317 + 'to use componentDidMount instead: Bar, Foo',
318 + 'Unsafe lifecycle methods were found within the following async tree:' +
319 + '\n in AsyncRootTwo (at **)' +
320 + '\n in div (at **)' +
321 + '\n in SyncRoot (at **)' +
322 + '\n\ncomponentWillMount: Please update the following components ' +
323 + 'to use componentDidMount instead: Baz',
324 + ]);
325 +
326 + // Dedupe
327 + rendered = ReactTestRenderer.create(<SyncRoot />);
328 + rendered.update(<SyncRoot />);
329 + });
330 +
331 + it('should warn about components not present during the initial render', () => {
332 + class AsyncRoot extends React.unstable_AsyncComponent {
333 + render() {
334 + return this.props.foo ? <Foo /> : <Bar />;
335 + }
336 + }
337 + class Foo extends React.Component {
338 + UNSAFE_componentWillMount() {}
339 + render() {
340 + return null;
341 + }
342 + }
343 + class Bar extends React.Component {
344 + UNSAFE_componentWillMount() {}
345 + render() {
346 + return null;
347 + }
348 + }
349 +
350 + let rendered;
351 + expect(() => {
352 + rendered = ReactTestRenderer.create(<AsyncRoot foo={true} />);
353 + }).toWarnDev(
354 + 'Unsafe lifecycle methods were found within the following async tree:' +
355 + '\n in AsyncRoot (at **)' +
356 + '\n\ncomponentWillMount: Please update the following components ' +
357 + 'to use componentDidMount instead: Foo' +
358 + '\n\nLearn more about this warning here:' +
359 + '\nhttps://fb.me/react-async-component-lifecycle-hooks',
360 + );
361 +
362 + expect(() => rendered.update(<AsyncRoot foo={false} />)).toWarnDev(
363 + 'Unsafe lifecycle methods were found within the following async tree:' +
364 + '\n in AsyncRoot (at **)' +
365 + '\n\ncomponentWillMount: Please update the following components ' +
366 + 'to use componentDidMount instead: Bar' +
367 + '\n\nLearn more about this warning here:' +
368 + '\nhttps://fb.me/react-async-component-lifecycle-hooks',
369 + );
370 +
371 + // Dedupe
372 + rendered.update(<AsyncRoot foo={true} />);
373 + rendered.update(<AsyncRoot foo={false} />);
374 + });
375 +
376 + it('should not warn about uncommitted lifecycles in the event of an error', () => {
377 + let caughtError;
378 +
379 + class AsyncRoot extends React.unstable_AsyncComponent {
380 + render() {
381 + return <ErrorBoundary />;
382 + }
383 + }
384 + class ErrorBoundary extends React.Component {
385 + state = {
386 + error: null,
387 + };
388 + componentDidCatch(error) {
389 + caughtError = error;
390 + this.setState({error});
391 + }
392 + render() {
393 + return this.state.error ? <Bar /> : <Foo />;
394 + }
395 + }
396 + class Foo extends React.Component {
397 + UNSAFE_componentWillMount() {}
398 + render() {
399 + throw Error('whoops');
400 + }
401 + }
402 + class Bar extends React.Component {
403 + UNSAFE_componentWillMount() {}
404 + render() {
405 + return null;
406 + }
407 + }
408 +
409 + expect(() => {
410 + ReactTestRenderer.create(<AsyncRoot foo={true} />);
411 + }).toWarnDev(
412 + 'Unsafe lifecycle methods were found within the following async tree:' +
413 + '\n in AsyncRoot (at **)' +
414 + '\n\ncomponentWillMount: Please update the following components ' +
415 + 'to use componentDidMount instead: Bar' +
416 + '\n\nLearn more about this warning here:' +
417 + '\nhttps://fb.me/react-async-component-lifecycle-hooks',
418 + );
419 +
420 + expect(caughtError).not.toBe(null);
421 + });
422 + });
423 });