Convert ReactCompositeComponentState to createRoot (#28063)
Ricky committed
Jan 25, 2024 at 13:36 UTC
0b32b3edd2c49b25fa25412fa6a6a31d43e15653
1 file changed
+247
-128
packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js
+247
-128
@@ -11,23 +11,41 @@
11
12
let React;
13
let ReactDOM;
14
-
14
+let ReactDOMClient;
15
+let act;
16
+let Scheduler;
17
+let assertLog;
18
let TestComponent;
19
+let testComponentInstance;
20
21
describe('ReactCompositeComponent-state', () => {
22
beforeEach(() => {
23
React = require('react');
24
ReactDOM = require('react-dom');
25
+ ReactDOMClient = require('react-dom/client');
26
+ act = require('internal-test-utils').act;
27
+ Scheduler = require('scheduler');
28
+
29
+ const InternalTestUtils = require('internal-test-utils');
30
+ assertLog = InternalTestUtils.assertLog;
31
+
32
+ function LogAfterCommit({children, color}) {
33
+ React.useEffect(() => {
34
+ Scheduler.log(`commit ${color}`);
35
+ });
36
+ return children;
37
+ }
38
39
TestComponent = class extends React.Component {
40
constructor(props) {
41
super(props);
42
this.peekAtState('getInitialState', undefined, props);
43
this.state = {color: 'red'};
44
+ testComponentInstance = this;
45
}
46
47
peekAtState = (from, state = this.state, props = this.props) => {
30
- props.stateListener(from, state && state.color);
48
+ Scheduler.log(`${from} ${state && state.color}`);
49
};
50
51
peekAtCallback = from => {
@@ -43,7 +61,11 @@ describe('ReactCompositeComponent-state', () => {
61
62
render() {
63
this.peekAtState('render');
46
- return <div>{this.state.color}</div>;
64
+ return (
65
+ <LogAfterCommit color={this.state.color}>
66
+ <div>{this.state.color}</div>
67
+ </LogAfterCommit>
68
+ );
69
}
70
71
UNSAFE_componentWillMount() {
@@ -121,105 +143,106 @@ describe('ReactCompositeComponent-state', () => {
143
};
144
});
145
124
- it('should support setting state', () => {
146
+ it('should support setting state', async () => {
147
const container = document.createElement('div');
148
document.body.appendChild(container);
149
+ const root = ReactDOMClient.createRoot(container);
150
128
- const stateListener = jest.fn();
129
- const instance = ReactDOM.render(
130
- <TestComponent stateListener={stateListener} />,
131
- container,
132
- function peekAtInitialCallback() {
133
- this.peekAtState('initial-callback');
134
- },
135
- );
136
- ReactDOM.render(
137
- <TestComponent stateListener={stateListener} nextColor="green" />,
138
- container,
139
- instance.peekAtCallback('setProps'),
140
- );
141
- instance.setFavoriteColor('blue');
142
- instance.forceUpdate(instance.peekAtCallback('forceUpdate'));
151
+ await act(() => {
152
+ root.render(<TestComponent />);
153
+ });
154
144
- ReactDOM.unmountComponentAtNode(container);
155
+ await act(() => {
156
+ root.render(<TestComponent nextColor="green" />);
157
+ });
158
146
- const expected = [
159
+ await act(() => {
160
+ testComponentInstance.setFavoriteColor('blue');
161
+ });
162
+ await act(() => {
163
+ testComponentInstance.forceUpdate(
164
+ testComponentInstance.peekAtCallback('forceUpdate'),
165
+ );
166
+ });
167
+
168
+ root.unmount();
169
+
170
+ assertLog([
171
// there is no state when getInitialState() is called
148
- ['getInitialState', null],
149
- ['componentWillMount-start', 'red'],
172
+ 'getInitialState undefined',
173
+ 'componentWillMount-start red',
174
// setState()'s only enqueue pending states.
151
- ['componentWillMount-after-sunrise', 'red'],
152
- ['componentWillMount-end', 'red'],
175
+ 'componentWillMount-after-sunrise red',
176
+ 'componentWillMount-end red',
177
// pending state queue is processed
154
- ['before-setState-sunrise', 'red'],
155
- ['after-setState-sunrise', 'sunrise'],
156
- ['after-setState-orange', 'orange'],
178
+ 'before-setState-sunrise red',
179
+ 'after-setState-sunrise sunrise',
180
+ 'after-setState-orange orange',
181
// pending state has been applied
158
- ['render', 'orange'],
159
- ['componentDidMount-start', 'orange'],
182
+ 'render orange',
183
+ 'componentDidMount-start orange',
184
// setState-sunrise and setState-orange should be called here,
185
// after the bug in #1740
186
// componentDidMount() called setState({color:'yellow'}), which is async.
187
// The update doesn't happen until the next flush.
164
- ['componentDidMount-end', 'orange'],
165
- ['setState-sunrise', 'orange'],
166
- ['setState-orange', 'orange'],
167
- ['initial-callback', 'orange'],
168
- ['shouldComponentUpdate-currentState', 'orange'],
169
- ['shouldComponentUpdate-nextState', 'yellow'],
170
- ['componentWillUpdate-currentState', 'orange'],
171
- ['componentWillUpdate-nextState', 'yellow'],
172
- ['render', 'yellow'],
173
- ['componentDidUpdate-currentState', 'yellow'],
174
- ['componentDidUpdate-prevState', 'orange'],
175
- ['setState-yellow', 'yellow'],
176
- ['componentWillReceiveProps-start', 'yellow'],
188
+ 'componentDidMount-end orange',
189
+ 'setState-sunrise orange',
190
+ 'setState-orange orange',
191
+ 'commit orange',
192
+ 'shouldComponentUpdate-currentState orange',
193
+ 'shouldComponentUpdate-nextState yellow',
194
+ 'componentWillUpdate-currentState orange',
195
+ 'componentWillUpdate-nextState yellow',
196
+ 'render yellow',
197
+ 'componentDidUpdate-currentState yellow',
198
+ 'componentDidUpdate-prevState orange',
199
+ 'setState-yellow yellow',
200
+ 'commit yellow',
201
+ 'componentWillReceiveProps-start yellow',
202
// setState({color:'green'}) only enqueues a pending state.
178
- ['componentWillReceiveProps-end', 'yellow'],
203
+ 'componentWillReceiveProps-end yellow',
204
// pending state queue is processed
205
// We keep updates in the queue to support
206
// replaceState(prevState => newState).
182
- ['before-setState-receiveProps', 'yellow'],
183
- ['before-setState-again-receiveProps', undefined],
184
- ['after-setState-receiveProps', 'green'],
185
- ['shouldComponentUpdate-currentState', 'yellow'],
186
- ['shouldComponentUpdate-nextState', 'green'],
187
- ['componentWillUpdate-currentState', 'yellow'],
188
- ['componentWillUpdate-nextState', 'green'],
189
- ['render', 'green'],
190
- ['componentDidUpdate-currentState', 'green'],
191
- ['componentDidUpdate-prevState', 'yellow'],
192
- ['setState-receiveProps', 'green'],
193
- ['setProps', 'green'],
207
+ 'before-setState-receiveProps yellow',
208
+ 'before-setState-again-receiveProps undefined',
209
+ 'after-setState-receiveProps green',
210
+ 'shouldComponentUpdate-currentState yellow',
211
+ 'shouldComponentUpdate-nextState green',
212
+ 'componentWillUpdate-currentState yellow',
213
+ 'componentWillUpdate-nextState green',
214
+ 'render green',
215
+ 'componentDidUpdate-currentState green',
216
+ 'componentDidUpdate-prevState yellow',
217
+ 'setState-receiveProps green',
218
+ 'commit green',
219
// setFavoriteColor('blue')
195
- ['shouldComponentUpdate-currentState', 'green'],
196
- ['shouldComponentUpdate-nextState', 'blue'],
197
- ['componentWillUpdate-currentState', 'green'],
198
- ['componentWillUpdate-nextState', 'blue'],
199
- ['render', 'blue'],
200
- ['componentDidUpdate-currentState', 'blue'],
201
- ['componentDidUpdate-prevState', 'green'],
202
- ['setFavoriteColor', 'blue'],
220
+ 'shouldComponentUpdate-currentState green',
221
+ 'shouldComponentUpdate-nextState blue',
222
+ 'componentWillUpdate-currentState green',
223
+ 'componentWillUpdate-nextState blue',
224
+ 'render blue',
225
+ 'componentDidUpdate-currentState blue',
226
+ 'componentDidUpdate-prevState green',
227
+ 'setFavoriteColor blue',
228
+ 'commit blue',
229
// forceUpdate()
204
- ['componentWillUpdate-currentState', 'blue'],
205
- ['componentWillUpdate-nextState', 'blue'],
206
- ['render', 'blue'],
207
- ['componentDidUpdate-currentState', 'blue'],
208
- ['componentDidUpdate-prevState', 'blue'],
209
- ['forceUpdate', 'blue'],
230
+ 'componentWillUpdate-currentState blue',
231
+ 'componentWillUpdate-nextState blue',
232
+ 'render blue',
233
+ 'componentDidUpdate-currentState blue',
234
+ 'componentDidUpdate-prevState blue',
235
+ 'forceUpdate blue',
236
+ 'commit blue',
237
// unmountComponent()
238
// state is available within `componentWillUnmount()`
212
- ['componentWillUnmount', 'blue'],
213
- ];
214
-
215
- expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n'));
239
+ 'componentWillUnmount blue',
240
+ ]);
241
});
242
218
- it('should call componentDidUpdate of children first', () => {
243
+ it('should call componentDidUpdate of children first', async () => {
244
const container = document.createElement('div');
245
221
- let ops = [];
222
-
246
let child = null;
247
let parent = null;
248
@@ -229,7 +252,7 @@ describe('ReactCompositeComponent-state', () => {
252
child = this;
253
}
254
componentDidUpdate() {
232
- ops.push('child did update');
255
+ Scheduler.log('child did update');
256
}
257
render() {
258
return <div />;
@@ -253,36 +276,40 @@ describe('ReactCompositeComponent-state', () => {
276
parent = this;
277
}
278
componentDidUpdate() {
256
- ops.push('parent did update');
279
+ Scheduler.log('parent did update');
280
}
281
render() {
282
return <Intermediate />;
283
}
284
}
285
263
- ReactDOM.render(<Parent />, container);
286
+ const root = ReactDOMClient.createRoot(container);
287
+ await act(() => {
288
+ root.render(<Parent />);
289
+ });
290
265
- ReactDOM.unstable_batchedUpdates(() => {
291
+ await act(() => {
292
parent.setState({foo: true});
293
child.setState({bar: true});
294
});
295
+
296
// When we render changes top-down in a batch, children's componentDidUpdate
297
// happens before the parent.
271
- expect(ops).toEqual(['child did update', 'parent did update']);
298
+ assertLog(['child did update', 'parent did update']);
299
300
shouldUpdate = false;
301
275
- ops = [];
276
-
277
- ReactDOM.unstable_batchedUpdates(() => {
302
+ await act(() => {
303
parent.setState({foo: false});
304
child.setState({bar: false});
305
});
306
+
307
// We expect the same thing to happen if we bail out in the middle.
282
- expect(ops).toEqual(['child did update', 'parent did update']);
308
+ assertLog(['child did update', 'parent did update']);
309
});
310
285
- it('should batch unmounts', () => {
311
+ it('should batch unmounts', async () => {
312
+ let outer;
313
class Inner extends React.Component {
314
render() {
315
return <div />;
@@ -297,6 +324,9 @@ describe('ReactCompositeComponent-state', () => {
324
325
class Outer extends React.Component {
326
state = {showInner: true};
327
+ componentDidMount() {
328
+ outer = this;
329
+ }
330
331
render() {
332
return <div>{this.state.showInner && <Inner />}</div>;
@@ -304,18 +334,21 @@ describe('ReactCompositeComponent-state', () => {
334
}
335
336
const container = document.createElement('div');
307
- const outer = ReactDOM.render(<Outer />, container);
337
+ const root = ReactDOMClient.createRoot(container);
338
+ await act(() => {
339
+ root.render(<Outer />);
340
+ });
341
+
342
expect(() => {
309
- ReactDOM.unmountComponentAtNode(container);
343
+ root.unmount();
344
}).not.toThrow();
345
});
346
313
- it('should update state when called from child cWRP', function () {
314
- const log = [];
347
+ it('should update state when called from child cWRP', async () => {
348
class Parent extends React.Component {
349
state = {value: 'one'};
350
render() {
318
- log.push('parent render ' + this.state.value);
351
+ Scheduler.log('parent render ' + this.state.value);
352
return <Child parent={this} value={this.state.value} />;
353
}
354
}
@@ -325,20 +358,28 @@ describe('ReactCompositeComponent-state', () => {
358
if (updated) {
359
return;
360
}
328
- log.push('child componentWillReceiveProps ' + this.props.value);
361
+ Scheduler.log('child componentWillReceiveProps ' + this.props.value);
362
this.props.parent.setState({value: 'two'});
330
- log.push('child componentWillReceiveProps done ' + this.props.value);
363
+ Scheduler.log(
364
+ 'child componentWillReceiveProps done ' + this.props.value,
365
+ );
366
updated = true;
367
}
368
render() {
334
- log.push('child render ' + this.props.value);
369
+ Scheduler.log('child render ' + this.props.value);
370
return <div>{this.props.value}</div>;
371
}
372
}
373
const container = document.createElement('div');
339
- ReactDOM.render(<Parent />, container);
340
- ReactDOM.render(<Parent />, container);
341
- expect(log).toEqual([
374
+ const root = ReactDOMClient.createRoot(container);
375
+ await act(() => {
376
+ root.render(<Parent />);
377
+ });
378
+ await act(() => {
379
+ root.render(<Parent />);
380
+ });
381
+
382
+ assertLog([
383
'parent render one',
384
'child render one',
385
'parent render one',
@@ -350,15 +391,19 @@ describe('ReactCompositeComponent-state', () => {
391
]);
392
});
393
353
- it('should merge state when sCU returns false', function () {
354
- const log = [];
394
+ it('should merge state when sCU returns false', async () => {
395
+ let test;
396
class Test extends React.Component {
397
state = {a: 0};
398
+ componentDidMount() {
399
+ test = this;
400
+ }
401
+
402
render() {
403
return null;
404
}
405
shouldComponentUpdate(nextProps, nextState) {
361
- log.push(
406
+ Scheduler.log(
407
'scu from ' +
408
Object.keys(this.state) +
409
' to ' +
@@ -369,22 +414,28 @@ describe('ReactCompositeComponent-state', () => {
414
}
415
416
const container = document.createElement('div');
372
- const test = ReactDOM.render(<Test />, container);
373
- test.setState({b: 0});
374
- expect(log.length).toBe(1);
375
- test.setState({c: 0});
376
- expect(log.length).toBe(2);
377
- expect(log).toEqual(['scu from a to a,b', 'scu from a,b to a,b,c']);
417
+ const root = ReactDOMClient.createRoot(container);
418
+ await act(() => {
419
+ root.render(<Test />);
420
+ });
421
+ await act(() => {
422
+ test.setState({b: 0});
423
+ });
424
+
425
+ assertLog(['scu from a to a,b']);
426
+ await act(() => {
427
+ test.setState({c: 0});
428
+ });
429
+ assertLog(['scu from a,b to a,b,c']);
430
});
431
380
- it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', () => {
381
- const ops = [];
432
+ it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', async () => {
433
class Test extends React.Component {
434
state = {step: 1, extra: true};
435
UNSAFE_componentWillReceiveProps() {
436
this.setState({step: 2}, () => {
437
// Tests that earlier setState callbacks are not dropped
387
- ops.push(
438
+ Scheduler.log(
439
`callback -- step: ${this.state.step}, extra: ${!!this.state
440
.extra}`,
441
);
@@ -393,7 +444,7 @@ describe('ReactCompositeComponent-state', () => {
444
this.state = {step: 3};
445
}
446
render() {
396
- ops.push(
447
+ Scheduler.log(
448
`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
449
);
450
return null;
@@ -402,32 +453,42 @@ describe('ReactCompositeComponent-state', () => {
453
454
// Mount
455
const container = document.createElement('div');
405
- ReactDOM.render(<Test />, container);
456
+ const root = ReactDOMClient.createRoot(container);
457
+ await act(() => {
458
+ root.render(<Test />);
459
+ });
460
// Update
407
- expect(() => ReactDOM.render(<Test />, container)).toErrorDev(
461
+ expect(() => {
462
+ ReactDOM.flushSync(() => {
463
+ root.render(<Test />);
464
+ });
465
+ }).toErrorDev(
466
'Warning: Test.componentWillReceiveProps(): Assigning directly to ' +
467
"this.state is deprecated (except inside a component's constructor). " +
468
'Use setState instead.',
469
);
470
413
- expect(ops).toEqual([
471
+ assertLog([
472
'render -- step: 1, extra: true',
473
'render -- step: 3, extra: false',
474
'callback -- step: 3, extra: false',
475
]);
476
477
// Check deduplication; (no additional warnings are expected)
420
- ReactDOM.render(<Test />, container);
478
+ expect(() => {
479
+ ReactDOM.flushSync(() => {
480
+ root.render(<Test />);
481
+ });
482
+ }).not.toThrow();
483
});
484
485
it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
424
- const ops = [];
486
class Test extends React.Component {
487
state = {step: 1, extra: true};
488
UNSAFE_componentWillMount() {
489
this.setState({step: 2}, () => {
490
// Tests that earlier setState callbacks are not dropped
430
- ops.push(
491
+ Scheduler.log(
492
`callback -- step: ${this.state.step}, extra: ${!!this.state
493
.extra}`,
494
);
@@ -436,7 +497,7 @@ describe('ReactCompositeComponent-state', () => {
497
this.state = {step: 3};
498
}
499
render() {
439
- ops.push(
500
+ Scheduler.log(
501
`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
502
);
503
return null;
@@ -445,20 +506,29 @@ describe('ReactCompositeComponent-state', () => {
506
507
// Mount
508
const container = document.createElement('div');
448
- expect(() => ReactDOM.render(<Test />, container)).toErrorDev(
509
+ const root = ReactDOMClient.createRoot(container);
510
+ expect(() => {
511
+ ReactDOM.flushSync(() => {
512
+ root.render(<Test />);
513
+ });
514
+ }).toErrorDev(
515
'Warning: Test.componentWillMount(): Assigning directly to ' +
516
"this.state is deprecated (except inside a component's constructor). " +
517
'Use setState instead.',
518
);
519
454
- expect(ops).toEqual([
520
+ assertLog([
521
+ 'render -- step: 3, extra: false',
522
+ 'callback -- step: 3, extra: false',
523
+
524
+ // A second time for the retry.
525
'render -- step: 3, extra: false',
526
'callback -- step: 3, extra: false',
527
]);
528
});
529
530
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
461
- it('should support stateful module pattern components', () => {
531
+ it('should support stateful module pattern components', async () => {
532
function Child() {
533
return {
534
state: {
@@ -471,7 +541,12 @@ describe('ReactCompositeComponent-state', () => {
541
}
542
543
const el = document.createElement('div');
474
- expect(() => ReactDOM.render(<Child />, el)).toErrorDev(
544
+ const root = ReactDOMClient.createRoot(el);
545
+ expect(() => {
546
+ ReactDOM.flushSync(() => {
547
+ root.render(<Child />);
548
+ });
549
+ }).toErrorDev(
550
'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
551
'Change Child to a class that extends React.Component instead. ' +
552
"If you can't use a class try assigning the prototype on the function as a workaround. " +
@@ -482,7 +557,7 @@ describe('ReactCompositeComponent-state', () => {
557
expect(el.textContent).toBe('count:123');
558
});
559
485
- it('should support getDerivedStateFromProps for module pattern components', () => {
560
+ it('should support getDerivedStateFromProps for module pattern components', async () => {
561
function Child() {
562
return {
563
state: {
@@ -500,18 +575,62 @@ describe('ReactCompositeComponent-state', () => {
575
};
576
577
const el = document.createElement('div');
503
- ReactDOM.render(<Child incrementBy={0} />, el);
504
- expect(el.textContent).toBe('count:1');
578
+ const root = ReactDOMClient.createRoot(el);
579
+ await act(() => {
580
+ root.render(<Child incrementBy={0} />);
581
+ });
582
506
- ReactDOM.render(<Child incrementBy={2} />, el);
583
+ expect(el.textContent).toBe('count:1');
584
+ await act(() => {
585
+ root.render(<Child incrementBy={2} />);
586
+ });
587
expect(el.textContent).toBe('count:3');
588
509
- ReactDOM.render(<Child incrementBy={1} />, el);
589
+ await act(() => {
590
+ root.render(<Child incrementBy={1} />);
591
+ });
592
expect(el.textContent).toBe('count:4');
593
});
594
}
595
514
- it('should support setState in componentWillUnmount', () => {
596
+ it('should not support setState in componentWillUnmount', async () => {
597
+ let subscription;
598
+ class A extends React.Component {
599
+ componentWillUnmount() {
600
+ subscription();
601
+ }
602
+ render() {
603
+ return 'A';
604
+ }
605
+ }
606
+
607
+ class B extends React.Component {
608
+ state = {siblingUnmounted: false};
609
+ UNSAFE_componentWillMount() {
610
+ subscription = () => this.setState({siblingUnmounted: true});
611
+ }
612
+ render() {
613
+ return 'B' + (this.state.siblingUnmounted ? ' No Sibling' : '');
614
+ }
615
+ }
616
+
617
+ const el = document.createElement('div');
618
+ const root = ReactDOMClient.createRoot(el);
619
+ await act(() => {
620
+ root.render(<A />);
621
+ });
622
+ expect(el.textContent).toBe('A');
623
+
624
+ expect(() => {
625
+ ReactDOM.flushSync(() => {
626
+ root.render(<B />);
627
+ });
628
+ }).toErrorDev(
629
+ "Warning: Can't perform a React state update on a component that hasn't mounted yet",
630
+ );
631
+ });
632
+
633
+ it('Legacy mode should support setState in componentWillUnmount (#18851)', () => {
634
let subscription;
635
class A extends React.Component {
636
componentWillUnmount() {