@samitouri / QOS-React-2 / commits / 0b32b3edd2

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() {