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
});