main
js 2,892 lines 79.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
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 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactNoop;
15 let Scheduler;
16 let PropTypes;
17
18 let assertConsoleErrorDev;
19 let waitForAll;
20 let waitFor;
21 let waitForThrow;
22 let assertLog;
23
24 describe('ReactIncremental', () => {
25 beforeEach(() => {
26 jest.resetModules();
27 React = require('react');
28 ReactNoop = require('react-noop-renderer');
29 Scheduler = require('scheduler');
30 PropTypes = require('prop-types');
31
32 ({
33 assertConsoleErrorDev,
34 waitForAll,
35 waitFor,
36 waitForThrow,
37 assertLog,
38 } = require('internal-test-utils'));
39 });
40
41 // Note: This is based on a similar component we use in www. We can delete
42 // once the extra div wrapper is no longer necessary.
43 function LegacyHiddenDiv({children, mode}) {
44 return (
45 <div hidden={mode === 'hidden'}>
46 <React.unstable_LegacyHidden
47 mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
48 {children}
49 </React.unstable_LegacyHidden>
50 </div>
51 );
52 }
53
54 it('should render a simple component', async () => {
55 function Bar() {
56 return <div>Hello World</div>;
57 }
58
59 function Foo() {
60 return <Bar isBar={true} />;
61 }
62
63 ReactNoop.render(<Foo />);
64 await waitForAll([]);
65 });
66
67 it('should render a simple component, in steps if needed', async () => {
68 function Bar() {
69 Scheduler.log('Bar');
70 return (
71 <span>
72 <div>Hello World</div>
73 </span>
74 );
75 }
76
77 function Foo() {
78 Scheduler.log('Foo');
79 return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
80 }
81
82 React.startTransition(() => {
83 ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
84 });
85 // Do one step of work.
86 await waitFor(['Foo']);
87
88 // Do the rest of the work.
89 await waitForAll(['Bar', 'Bar', 'callback']);
90 });
91
92 it('updates a previous render', async () => {
93 function Header() {
94 Scheduler.log('Header');
95 return <h1>Hi</h1>;
96 }
97
98 function Content(props) {
99 Scheduler.log('Content');
100 return <div>{props.children}</div>;
101 }
102
103 function Footer() {
104 Scheduler.log('Footer');
105 return <footer>Bye</footer>;
106 }
107
108 const header = <Header />;
109 const footer = <Footer />;
110
111 function Foo(props) {
112 Scheduler.log('Foo');
113 return (
114 <div>
115 {header}
116 <Content>{props.text}</Content>
117 {footer}
118 </div>
119 );
120 }
121
122 ReactNoop.render(<Foo text="foo" />, () =>
123 Scheduler.log('renderCallbackCalled'),
124 );
125 await waitForAll([
126 'Foo',
127 'Header',
128 'Content',
129 'Footer',
130 'renderCallbackCalled',
131 ]);
132
133 ReactNoop.render(<Foo text="bar" />, () =>
134 Scheduler.log('firstRenderCallbackCalled'),
135 );
136 ReactNoop.render(<Foo text="bar" />, () =>
137 Scheduler.log('secondRenderCallbackCalled'),
138 );
139 // TODO: Test bail out of host components. This is currently unobservable.
140
141 // Since this is an update, it should bail out and reuse the work from
142 // Header and Content.
143 await waitForAll([
144 'Foo',
145 'Content',
146 'firstRenderCallbackCalled',
147 'secondRenderCallbackCalled',
148 ]);
149 });
150
151 it('can cancel partially rendered work and restart', async () => {
152 function Bar(props) {
153 Scheduler.log('Bar');
154 return <div>{props.children}</div>;
155 }
156
157 function Foo(props) {
158 Scheduler.log('Foo');
159 return (
160 <div>
161 <Bar>{props.text}</Bar>
162 <Bar>{props.text}</Bar>
163 </div>
164 );
165 }
166
167 // Init
168 ReactNoop.render(<Foo text="foo" />);
169 await waitForAll(['Foo', 'Bar', 'Bar']);
170
171 React.startTransition(() => {
172 ReactNoop.render(<Foo text="bar" />);
173 });
174 // Flush part of the work
175 await waitFor(['Foo', 'Bar']);
176
177 // This will abort the previous work and restart
178 ReactNoop.flushSync(() => ReactNoop.render(null));
179
180 React.startTransition(() => {
181 ReactNoop.render(<Foo text="baz" />);
182 });
183
184 // Flush part of the new work
185 await waitFor(['Foo', 'Bar']);
186
187 // Flush the rest of the work which now includes the low priority
188 await waitForAll(['Bar']);
189 });
190
191 it('should call callbacks even if updates are aborted', async () => {
192 let inst;
193
194 class Foo extends React.Component {
195 constructor(props) {
196 super(props);
197 this.state = {
198 text: 'foo',
199 text2: 'foo',
200 };
201 inst = this;
202 }
203 render() {
204 return (
205 <div>
206 <div>{this.state.text}</div>
207 <div>{this.state.text2}</div>
208 </div>
209 );
210 }
211 }
212
213 ReactNoop.render(<Foo />);
214 await waitForAll([]);
215
216 React.startTransition(() => {
217 inst.setState(
218 () => {
219 Scheduler.log('setState1');
220 return {text: 'bar'};
221 },
222 () => Scheduler.log('callback1'),
223 );
224 });
225
226 // Flush part of the work
227 await waitFor(['setState1']);
228
229 // This will abort the previous work and restart
230 ReactNoop.flushSync(() => ReactNoop.render(<Foo />));
231 React.startTransition(() => {
232 inst.setState(
233 () => {
234 Scheduler.log('setState2');
235 return {text2: 'baz'};
236 },
237 () => Scheduler.log('callback2'),
238 );
239 });
240
241 // Flush the rest of the work which now includes the low priority
242 await waitForAll(['setState1', 'setState2', 'callback1', 'callback2']);
243 expect(inst.state).toEqual({text: 'bar', text2: 'baz'});
244 });
245
246 // @gate enableLegacyHidden
247 it('can deprioritize unfinished work and resume it later', async () => {
248 function Bar(props) {
249 Scheduler.log('Bar');
250 return <div>{props.children}</div>;
251 }
252
253 function Middle(props) {
254 Scheduler.log('Middle');
255 return <span>{props.children}</span>;
256 }
257
258 function Foo(props) {
259 Scheduler.log('Foo');
260 return (
261 <div>
262 <Bar>{props.text}</Bar>
263 <LegacyHiddenDiv mode="hidden">
264 <Middle>{props.text}</Middle>
265 </LegacyHiddenDiv>
266 <Bar>{props.text}</Bar>
267 <LegacyHiddenDiv mode="hidden">
268 <Middle>Footer</Middle>
269 </LegacyHiddenDiv>
270 </div>
271 );
272 }
273
274 // Init
275 ReactNoop.render(<Foo text="foo" />);
276 await waitForAll(['Foo', 'Bar', 'Bar', 'Middle', 'Middle']);
277
278 // Render part of the work. This should be enough to flush everything except
279 // the middle which has lower priority.
280 ReactNoop.render(<Foo text="bar" />);
281 await waitFor(['Foo', 'Bar', 'Bar']);
282 // Flush only the remaining work
283 await waitForAll(['Middle', 'Middle']);
284 });
285
286 // @gate enableLegacyHidden
287 it('can deprioritize a tree from without dropping work', async () => {
288 function Bar(props) {
289 Scheduler.log('Bar');
290 return <div>{props.children}</div>;
291 }
292
293 function Middle(props) {
294 Scheduler.log('Middle');
295 return <span>{props.children}</span>;
296 }
297
298 function Foo(props) {
299 Scheduler.log('Foo');
300 return (
301 <div>
302 <Bar>{props.text}</Bar>
303 <LegacyHiddenDiv mode="hidden">
304 <Middle>{props.text}</Middle>
305 </LegacyHiddenDiv>
306 <Bar>{props.text}</Bar>
307 <LegacyHiddenDiv mode="hidden">
308 <Middle>Footer</Middle>
309 </LegacyHiddenDiv>
310 </div>
311 );
312 }
313
314 // Init
315 ReactNoop.flushSync(() => {
316 ReactNoop.render(<Foo text="foo" />);
317 });
318 assertLog(['Foo', 'Bar', 'Bar']);
319 await waitForAll(['Middle', 'Middle']);
320
321 // Render the high priority work (everything except the hidden trees).
322 ReactNoop.flushSync(() => {
323 ReactNoop.render(<Foo text="foo" />);
324 });
325 assertLog(['Foo', 'Bar', 'Bar']);
326
327 // The hidden content was deprioritized from high to low priority. A low
328 // priority callback should have been scheduled. Flush it now.
329 await waitForAll(['Middle', 'Middle']);
330 });
331
332 // eslint-disable-next-line jest/no-disabled-tests
333 it.skip('can resume work in a subtree even when a parent bails out', async () => {
334 function Bar(props) {
335 Scheduler.log('Bar');
336 return <div>{props.children}</div>;
337 }
338
339 function Tester() {
340 // This component is just here to ensure that the bail out is
341 // in fact in effect in the expected place for this test.
342 Scheduler.log('Tester');
343 return <div />;
344 }
345
346 function Middle(props) {
347 Scheduler.log('Middle');
348 return <span>{props.children}</span>;
349 }
350
351 const middleContent = (
352 <aaa>
353 <Tester />
354 <bbb hidden={true}>
355 <ccc>
356 <Middle>Hi</Middle>
357 </ccc>
358 </bbb>
359 </aaa>
360 );
361
362 function Foo(props) {
363 Scheduler.log('Foo');
364 return (
365 <div>
366 <Bar>{props.text}</Bar>
367 {middleContent}
368 <Bar>{props.text}</Bar>
369 </div>
370 );
371 }
372
373 // Init
374 ReactNoop.render(<Foo text="foo" />);
375 ReactNoop.flushDeferredPri(52);
376
377 assertLog(['Foo', 'Bar', 'Tester', 'Bar']);
378
379 // We're now rendering an update that will bail out on updating middle.
380 ReactNoop.render(<Foo text="bar" />);
381 ReactNoop.flushDeferredPri(45 + 5);
382
383 assertLog(['Foo', 'Bar', 'Bar']);
384
385 // Flush the rest to make sure that the bailout didn't block this work.
386 await waitForAll(['Middle']);
387 });
388
389 // eslint-disable-next-line jest/no-disabled-tests
390 it.skip('can resume work in a bailed subtree within one pass', async () => {
391 function Bar(props) {
392 Scheduler.log('Bar');
393 return <div>{props.children}</div>;
394 }
395
396 class Tester extends React.Component {
397 shouldComponentUpdate() {
398 return false;
399 }
400 render() {
401 // This component is just here to ensure that the bail out is
402 // in fact in effect in the expected place for this test.
403 Scheduler.log('Tester');
404 return <div />;
405 }
406 }
407
408 function Middle(props) {
409 Scheduler.log('Middle');
410 return <span>{props.children}</span>;
411 }
412
413 // Should content not just bail out on current, not workInProgress?
414
415 class Content extends React.Component {
416 shouldComponentUpdate() {
417 return false;
418 }
419 render() {
420 return [
421 <Tester key="a" unused={this.props.unused} />,
422 <bbb key="b" hidden={true}>
423 <ccc>
424 <Middle>Hi</Middle>
425 </ccc>
426 </bbb>,
427 ];
428 }
429 }
430
431 function Foo(props) {
432 Scheduler.log('Foo');
433 return (
434 <div hidden={props.text === 'bar'}>
435 <Bar>{props.text}</Bar>
436 <Content unused={props.text} />
437 <Bar>{props.text}</Bar>
438 </div>
439 );
440 }
441
442 // Init
443 ReactNoop.render(<Foo text="foo" />);
444 ReactNoop.flushDeferredPri(52 + 5);
445
446 assertLog(['Foo', 'Bar', 'Tester', 'Bar']);
447
448 // Make a quick update which will create a low pri tree on top of the
449 // already low pri tree.
450 ReactNoop.render(<Foo text="bar" />);
451 ReactNoop.flushDeferredPri(15);
452
453 assertLog(['Foo']);
454
455 // At this point, middle will bail out but it has not yet fully rendered.
456 // Since that is the same priority as its parent tree. This should render
457 // as a single batch. Therefore, it is correct that Middle should be in the
458 // middle. If it occurs after the two "Bar" components then it was flushed
459 // after them which is not correct.
460 await waitForAll(['Bar', 'Middle', 'Bar']);
461
462 // Let us try this again without fully finishing the first time. This will
463 // create a hanging subtree that is reconciling at the normal priority.
464 ReactNoop.render(<Foo text="foo" />);
465 ReactNoop.flushDeferredPri(40);
466
467 assertLog(['Foo', 'Bar']);
468
469 // This update will create a tree that aborts that work and down-prioritizes
470 // it. If the priority levels aren't down-prioritized correctly this may
471 // abort rendering of the down-prioritized content.
472 ReactNoop.render(<Foo text="bar" />);
473 await waitForAll(['Foo', 'Bar', 'Bar']);
474 });
475
476 // eslint-disable-next-line jest/no-disabled-tests
477 it.skip('can resume mounting a class component', async () => {
478 let foo;
479 class Parent extends React.Component {
480 shouldComponentUpdate() {
481 return false;
482 }
483 render() {
484 return <Foo prop={this.props.prop} />;
485 }
486 }
487
488 class Foo extends React.Component {
489 constructor(props) {
490 super(props);
491 // Test based on a www bug where props was null on resume
492 Scheduler.log('Foo constructor: ' + props.prop);
493 }
494 render() {
495 foo = this;
496 Scheduler.log('Foo');
497 return <Bar />;
498 }
499 }
500
501 function Bar() {
502 Scheduler.log('Bar');
503 return <div />;
504 }
505
506 ReactNoop.render(<Parent prop="foo" />);
507 ReactNoop.flushDeferredPri(20);
508 assertLog(['Foo constructor: foo', 'Foo']);
509
510 foo.setState({value: 'bar'});
511
512 await waitForAll(['Foo', 'Bar']);
513 });
514
515 // eslint-disable-next-line jest/no-disabled-tests
516 it.skip('reuses the same instance when resuming a class instance', async () => {
517 let foo;
518 class Parent extends React.Component {
519 shouldComponentUpdate() {
520 return false;
521 }
522 render() {
523 return <Foo prop={this.props.prop} />;
524 }
525 }
526
527 let constructorCount = 0;
528 class Foo extends React.Component {
529 constructor(props) {
530 super(props);
531 // Test based on a www bug where props was null on resume
532 Scheduler.log('constructor: ' + props.prop);
533 constructorCount++;
534 }
535 UNSAFE_componentWillMount() {
536 Scheduler.log('componentWillMount: ' + this.props.prop);
537 }
538 UNSAFE_componentWillReceiveProps() {
539 Scheduler.log('componentWillReceiveProps: ' + this.props.prop);
540 }
541 componentDidMount() {
542 Scheduler.log('componentDidMount: ' + this.props.prop);
543 }
544 UNSAFE_componentWillUpdate() {
545 Scheduler.log('componentWillUpdate: ' + this.props.prop);
546 }
547 componentDidUpdate() {
548 Scheduler.log('componentDidUpdate: ' + this.props.prop);
549 }
550 render() {
551 foo = this;
552 Scheduler.log('render: ' + this.props.prop);
553 return <Bar />;
554 }
555 }
556
557 function Bar() {
558 Scheduler.log('Foo did complete');
559 return <div />;
560 }
561
562 ReactNoop.render(<Parent prop="foo" />);
563 ReactNoop.flushDeferredPri(25);
564 assertLog([
565 'constructor: foo',
566 'componentWillMount: foo',
567 'render: foo',
568 'Foo did complete',
569 ]);
570
571 foo.setState({value: 'bar'});
572
573 await waitForAll([]);
574 expect(constructorCount).toEqual(1);
575 assertLog([
576 'componentWillMount: foo',
577 'render: foo',
578 'Foo did complete',
579 'componentDidMount: foo',
580 ]);
581 });
582
583 // eslint-disable-next-line jest/no-disabled-tests
584 it.skip('can reuse work done after being preempted', async () => {
585 function Bar(props) {
586 Scheduler.log('Bar');
587 return <div>{props.children}</div>;
588 }
589
590 function Middle(props) {
591 Scheduler.log('Middle');
592 return <span>{props.children}</span>;
593 }
594
595 const middleContent = (
596 <div>
597 <Middle>Hello</Middle>
598 <Bar>-</Bar>
599 <Middle>World</Middle>
600 </div>
601 );
602
603 const step0 = (
604 <div>
605 <Middle>Hi</Middle>
606 <Bar>{'Foo'}</Bar>
607 <Middle>There</Middle>
608 </div>
609 );
610
611 function Foo(props) {
612 Scheduler.log('Foo');
613 return (
614 <div>
615 <Bar>{props.text2}</Bar>
616 <div hidden={true}>{props.step === 0 ? step0 : middleContent}</div>
617 </div>
618 );
619 }
620
621 // Init
622 ReactNoop.render(<Foo text="foo" text2="foo" step={0} />);
623 ReactNoop.flushDeferredPri(55 + 25 + 5 + 5);
624
625 // We only finish the higher priority work. So the low pri content
626 // has not yet finished mounting.
627 assertLog(['Foo', 'Bar', 'Middle', 'Bar']);
628
629 // Interrupt the rendering with a quick update. This should not touch the
630 // middle content.
631 ReactNoop.render(<Foo text="foo" text2="bar" step={0} />);
632 await waitForAll([]);
633
634 // We've now rendered the entire tree but we didn't have to redo the work
635 // done by the first Middle and Bar already.
636 assertLog(['Foo', 'Bar', 'Middle']);
637
638 // Make a quick update which will schedule low priority work to
639 // update the middle content.
640 ReactNoop.render(<Foo text="bar" text2="bar" step={1} />);
641 ReactNoop.flushDeferredPri(30 + 25 + 5);
642
643 assertLog(['Foo', 'Bar']);
644
645 // The middle content is now pending rendering...
646 ReactNoop.flushDeferredPri(30 + 5);
647 assertLog(['Middle', 'Bar']);
648
649 // but we'll interrupt it to render some higher priority work.
650 // The middle content will bailout so it remains untouched.
651 ReactNoop.render(<Foo text="foo" text2="bar" step={1} />);
652 ReactNoop.flushDeferredPri(30);
653
654 assertLog(['Foo', 'Bar']);
655
656 // Since we did nothing to the middle subtree during the interruption,
657 // we should be able to reuse the reconciliation work that we already did
658 // without restarting.
659 await waitForAll(['Middle']);
660 });
661
662 // eslint-disable-next-line jest/no-disabled-tests
663 it.skip('can reuse work that began but did not complete, after being preempted', async () => {
664 let child;
665 let sibling;
666
667 function GreatGrandchild() {
668 Scheduler.log('GreatGrandchild');
669 return <div />;
670 }
671
672 function Grandchild() {
673 Scheduler.log('Grandchild');
674 return <GreatGrandchild />;
675 }
676
677 class Child extends React.Component {
678 state = {step: 0};
679 render() {
680 child = this;
681 Scheduler.log('Child');
682 return <Grandchild />;
683 }
684 }
685
686 class Sibling extends React.Component {
687 render() {
688 Scheduler.log('Sibling');
689 sibling = this;
690 return <div />;
691 }
692 }
693
694 function Parent() {
695 Scheduler.log('Parent');
696 return [
697 // The extra div is necessary because when Parent bails out during the
698 // high priority update, its progressedPriority is set to high.
699 // So its direct children cannot be reused when we resume at
700 // low priority. I think this would be fixed by changing
701 // pendingWorkPriority and progressedPriority to be the priority of
702 // the children only, not including the fiber itself.
703 <div key="a">
704 <Child />
705 </div>,
706 <Sibling key="b" />,
707 ];
708 }
709
710 ReactNoop.render(<Parent />);
711 await waitForAll([]);
712
713 // Begin working on a low priority update to Child, but stop before
714 // GreatGrandchild. Child and Grandchild begin but don't complete.
715 child.setState({step: 1});
716 ReactNoop.flushDeferredPri(30);
717 assertLog(['Child', 'Grandchild']);
718
719 // Interrupt the current low pri work with a high pri update elsewhere in
720 // the tree.
721
722 ReactNoop.flushSync(() => {
723 sibling.setState({});
724 });
725 assertLog(['Sibling']);
726
727 // Continue the low pri work. The work on Child and GrandChild was memoized
728 // so they should not be worked on again.
729
730 await waitForAll([
731 // No Child
732 // No Grandchild
733 'GreatGrandchild',
734 ]);
735 });
736
737 // eslint-disable-next-line jest/no-disabled-tests
738 it.skip('can reuse work if shouldComponentUpdate is false, after being preempted', async () => {
739 function Bar(props) {
740 Scheduler.log('Bar');
741 return <div>{props.children}</div>;
742 }
743
744 class Middle extends React.Component {
745 shouldComponentUpdate(nextProps) {
746 return this.props.children !== nextProps.children;
747 }
748 render() {
749 Scheduler.log('Middle');
750 return <span>{this.props.children}</span>;
751 }
752 }
753
754 class Content extends React.Component {
755 shouldComponentUpdate(nextProps) {
756 return this.props.step !== nextProps.step;
757 }
758 render() {
759 Scheduler.log('Content');
760 return (
761 <div>
762 <Middle>{this.props.step === 0 ? 'Hi' : 'Hello'}</Middle>
763 <Bar>{this.props.step === 0 ? this.props.text : '-'}</Bar>
764 <Middle>{this.props.step === 0 ? 'There' : 'World'}</Middle>
765 </div>
766 );
767 }
768 }
769
770 function Foo(props) {
771 Scheduler.log('Foo');
772 return (
773 <div>
774 <Bar>{props.text}</Bar>
775 <div hidden={true}>
776 <Content step={props.step} text={props.text} />
777 </div>
778 </div>
779 );
780 }
781
782 // Init
783 ReactNoop.render(<Foo text="foo" step={0} />);
784 await waitForAll(['Foo', 'Bar', 'Content', 'Middle', 'Bar', 'Middle']);
785
786 // Make a quick update which will schedule low priority work to
787 // update the middle content.
788 ReactNoop.render(<Foo text="bar" step={1} />);
789 ReactNoop.flushDeferredPri(30 + 5);
790
791 assertLog(['Foo', 'Bar']);
792
793 // The middle content is now pending rendering...
794 ReactNoop.flushDeferredPri(30 + 25 + 5);
795 assertLog(['Content', 'Middle', 'Bar']); // One more Middle left.
796
797 // but we'll interrupt it to render some higher priority work.
798 // The middle content will bailout so it remains untouched.
799 ReactNoop.render(<Foo text="foo" step={1} />);
800 ReactNoop.flushDeferredPri(30);
801
802 assertLog(['Foo', 'Bar']);
803
804 // Since we did nothing to the middle subtree during the interruption,
805 // we should be able to reuse the reconciliation work that we already did
806 // without restarting.
807 await waitForAll(['Middle']);
808 });
809
810 it('memoizes work even if shouldComponentUpdate returns false', async () => {
811 class Foo extends React.Component {
812 shouldComponentUpdate(nextProps) {
813 // this.props is the memoized props. So this should return true for
814 // every update except the first one.
815 const shouldUpdate = this.props.step !== 1;
816 Scheduler.log('shouldComponentUpdate: ' + shouldUpdate);
817 return shouldUpdate;
818 }
819 render() {
820 Scheduler.log('render');
821 return <div />;
822 }
823 }
824
825 ReactNoop.render(<Foo step={1} />);
826 await waitForAll(['render']);
827
828 ReactNoop.render(<Foo step={2} />);
829 await waitForAll(['shouldComponentUpdate: false']);
830
831 ReactNoop.render(<Foo step={3} />);
832 await waitForAll([
833 // If the memoized props were not updated during last bail out, sCU will
834 // keep returning false.
835 'shouldComponentUpdate: true',
836 'render',
837 ]);
838 });
839
840 it('can update in the middle of a tree using setState', async () => {
841 let instance;
842 class Bar extends React.Component {
843 constructor() {
844 super();
845 this.state = {a: 'a'};
846 instance = this;
847 }
848 render() {
849 return <div>{this.props.children}</div>;
850 }
851 }
852
853 function Foo() {
854 return (
855 <div>
856 <Bar />
857 </div>
858 );
859 }
860
861 ReactNoop.render(<Foo />);
862 await waitForAll([]);
863 expect(instance.state).toEqual({a: 'a'});
864 instance.setState({b: 'b'});
865 await waitForAll([]);
866 expect(instance.state).toEqual({a: 'a', b: 'b'});
867 });
868
869 it('can queue multiple state updates', async () => {
870 let instance;
871 class Bar extends React.Component {
872 constructor() {
873 super();
874 this.state = {a: 'a'};
875 instance = this;
876 }
877 render() {
878 return <div>{this.props.children}</div>;
879 }
880 }
881
882 function Foo() {
883 return (
884 <div>
885 <Bar />
886 </div>
887 );
888 }
889
890 ReactNoop.render(<Foo />);
891 await waitForAll([]);
892 // Call setState multiple times before flushing
893 instance.setState({b: 'b'});
894 instance.setState({c: 'c'});
895 instance.setState({d: 'd'});
896 await waitForAll([]);
897 expect(instance.state).toEqual({a: 'a', b: 'b', c: 'c', d: 'd'});
898 });
899
900 it('can use updater form of setState', async () => {
901 let instance;
902 class Bar extends React.Component {
903 constructor() {
904 super();
905 this.state = {num: 1};
906 instance = this;
907 }
908 render() {
909 return <div>{this.props.children}</div>;
910 }
911 }
912
913 function Foo({multiplier}) {
914 return (
915 <div>
916 <Bar multiplier={multiplier} />
917 </div>
918 );
919 }
920
921 function updater(state, props) {
922 return {num: state.num * props.multiplier};
923 }
924
925 ReactNoop.render(<Foo multiplier={2} />);
926 await waitForAll([]);
927 expect(instance.state.num).toEqual(1);
928 instance.setState(updater);
929 await waitForAll([]);
930 expect(instance.state.num).toEqual(2);
931
932 instance.setState(updater);
933 ReactNoop.render(<Foo multiplier={3} />);
934 await waitForAll([]);
935 expect(instance.state.num).toEqual(6);
936 });
937
938 it('can call setState inside update callback', async () => {
939 let instance;
940 class Bar extends React.Component {
941 constructor() {
942 super();
943 this.state = {num: 1};
944 instance = this;
945 }
946 render() {
947 return <div>{this.props.children}</div>;
948 }
949 }
950
951 function Foo({multiplier}) {
952 return (
953 <div>
954 <Bar multiplier={multiplier} />
955 </div>
956 );
957 }
958
959 function updater(state, props) {
960 return {num: state.num * props.multiplier};
961 }
962
963 function callback() {
964 this.setState({called: true});
965 }
966
967 ReactNoop.render(<Foo multiplier={2} />);
968 await waitForAll([]);
969 instance.setState(updater);
970 instance.setState(updater, callback);
971 await waitForAll([]);
972 expect(instance.state.num).toEqual(4);
973 expect(instance.state.called).toEqual(true);
974 });
975
976 it('can replaceState', async () => {
977 let instance;
978 class Bar extends React.Component {
979 state = {a: 'a'};
980 render() {
981 instance = this;
982 return <div>{this.props.children}</div>;
983 }
984 }
985
986 function Foo() {
987 return (
988 <div>
989 <Bar />
990 </div>
991 );
992 }
993
994 ReactNoop.render(<Foo />);
995 await waitForAll([]);
996 instance.setState({b: 'b'});
997 instance.setState({c: 'c'});
998 instance.updater.enqueueReplaceState(instance, {d: 'd'});
999 await waitForAll([]);
1000 expect(instance.state).toEqual({d: 'd'});
1001 });
1002
1003 it('can forceUpdate', async () => {
1004 function Baz() {
1005 Scheduler.log('Baz');
1006 return <div />;
1007 }
1008
1009 let instance;
1010 class Bar extends React.Component {
1011 constructor() {
1012 super();
1013 instance = this;
1014 }
1015 shouldComponentUpdate() {
1016 return false;
1017 }
1018 render() {
1019 Scheduler.log('Bar');
1020 return <Baz />;
1021 }
1022 }
1023
1024 function Foo() {
1025 Scheduler.log('Foo');
1026 return (
1027 <div>
1028 <Bar />
1029 </div>
1030 );
1031 }
1032
1033 ReactNoop.render(<Foo />);
1034 await waitForAll(['Foo', 'Bar', 'Baz']);
1035 instance.forceUpdate();
1036 await waitForAll(['Bar', 'Baz']);
1037 });
1038
1039 it('should clear forceUpdate after update is flushed', async () => {
1040 let a = 0;
1041
1042 class Foo extends React.PureComponent {
1043 render() {
1044 const msg = `A: ${a}, B: ${this.props.b}`;
1045 Scheduler.log(msg);
1046 return msg;
1047 }
1048 }
1049
1050 const foo = React.createRef(null);
1051 ReactNoop.render(<Foo ref={foo} b={0} />);
1052 await waitForAll(['A: 0, B: 0']);
1053
1054 a = 1;
1055 foo.current.forceUpdate();
1056 await waitForAll(['A: 1, B: 0']);
1057
1058 ReactNoop.render(<Foo ref={foo} b={0} />);
1059 await waitForAll([]);
1060 });
1061
1062 // eslint-disable-next-line jest/no-disabled-tests
1063 it.skip('can call sCU while resuming a partly mounted component', () => {
1064 const instances = new Set();
1065
1066 class Bar extends React.Component {
1067 state = {y: 'A'};
1068 constructor() {
1069 super();
1070 instances.add(this);
1071 }
1072 shouldComponentUpdate(newProps, newState) {
1073 return this.props.x !== newProps.x || this.state.y !== newState.y;
1074 }
1075 render() {
1076 Scheduler.log('Bar:' + this.props.x);
1077 return <span prop={String(this.props.x === this.state.y)} />;
1078 }
1079 }
1080
1081 function Foo(props) {
1082 Scheduler.log('Foo');
1083 return [
1084 <Bar key="a" x="A" />,
1085 <Bar key="b" x={props.step === 0 ? 'B' : 'B2'} />,
1086 <Bar key="c" x="C" />,
1087 <Bar key="d" x="D" />,
1088 ];
1089 }
1090
1091 ReactNoop.render(<Foo step={0} />);
1092 ReactNoop.flushDeferredPri(40);
1093 assertLog(['Foo', 'Bar:A', 'Bar:B', 'Bar:C']);
1094
1095 expect(instances.size).toBe(3);
1096
1097 ReactNoop.render(<Foo step={1} />);
1098 ReactNoop.flushDeferredPri(50);
1099 // A was memoized and reused. B was memoized but couldn't be reused because
1100 // props differences. C was memoized and reused. D never even started so it
1101 // needed a new instance.
1102 assertLog(['Foo', 'Bar:B2', 'Bar:D']);
1103
1104 // We expect each rerender to correspond to a new instance.
1105 expect(instances.size).toBe(4);
1106 });
1107
1108 // eslint-disable-next-line jest/no-disabled-tests
1109 it.skip('gets new props when setting state on a partly updated component', async () => {
1110 const instances = [];
1111
1112 class Bar extends React.Component {
1113 state = {y: 'A'};
1114 constructor() {
1115 super();
1116 instances.push(this);
1117 }
1118 performAction() {
1119 this.setState({
1120 y: 'B',
1121 });
1122 }
1123 render() {
1124 Scheduler.log('Bar:' + this.props.x + '-' + this.props.step);
1125 return <span prop={String(this.props.x === this.state.y)} />;
1126 }
1127 }
1128
1129 function Baz() {
1130 // This component is used as a sibling to Foo so that we can fully
1131 // complete Foo, without committing.
1132 Scheduler.log('Baz');
1133 return <div />;
1134 }
1135
1136 function Foo(props) {
1137 Scheduler.log('Foo');
1138 return [
1139 <Bar key="a" x="A" step={props.step} />,
1140 <Bar key="b" x="B" step={props.step} />,
1141 ];
1142 }
1143
1144 ReactNoop.render(
1145 <div>
1146 <Foo step={0} />
1147 <Baz />
1148 <Baz />
1149 </div>,
1150 );
1151 await waitForAll([]);
1152
1153 // Flush part way through with new props, fully completing the first Bar.
1154 // However, it doesn't commit yet.
1155 ReactNoop.render(
1156 <div>
1157 <Foo step={1} />
1158 <Baz />
1159 <Baz />
1160 </div>,
1161 );
1162 ReactNoop.flushDeferredPri(45);
1163 assertLog(['Foo', 'Bar:A-1', 'Bar:B-1', 'Baz']);
1164
1165 // Make an update to the same Bar.
1166 instances[0].performAction();
1167
1168 await waitForAll(['Bar:A-1', 'Baz']);
1169 });
1170
1171 // eslint-disable-next-line jest/no-disabled-tests
1172 it.skip('calls componentWillMount twice if the initial render is aborted', async () => {
1173 class LifeCycle extends React.Component {
1174 state = {x: this.props.x};
1175 UNSAFE_componentWillReceiveProps(nextProps) {
1176 Scheduler.log(
1177 'componentWillReceiveProps:' + this.state.x + '-' + nextProps.x,
1178 );
1179 this.setState({x: nextProps.x});
1180 }
1181 UNSAFE_componentWillMount() {
1182 Scheduler.log(
1183 'componentWillMount:' + this.state.x + '-' + this.props.x,
1184 );
1185 }
1186 componentDidMount() {
1187 Scheduler.log('componentDidMount:' + this.state.x + '-' + this.props.x);
1188 }
1189 render() {
1190 return <span />;
1191 }
1192 }
1193
1194 function Trail() {
1195 Scheduler.log('Trail');
1196 return null;
1197 }
1198
1199 function App(props) {
1200 Scheduler.log('App');
1201 return (
1202 <div>
1203 <LifeCycle x={props.x} />
1204 <Trail />
1205 </div>
1206 );
1207 }
1208
1209 ReactNoop.render(<App x={0} />);
1210 ReactNoop.flushDeferredPri(30);
1211
1212 assertLog(['App', 'componentWillMount:0-0']);
1213
1214 ReactNoop.render(<App x={1} />);
1215 await waitForAll([
1216 'App',
1217 'componentWillReceiveProps:0-1',
1218 'componentWillMount:1-1',
1219 'Trail',
1220 'componentDidMount:1-1',
1221 ]);
1222 });
1223
1224 // eslint-disable-next-line jest/no-disabled-tests
1225 it.skip('uses state set in componentWillMount even if initial render was aborted', async () => {
1226 class LifeCycle extends React.Component {
1227 constructor(props) {
1228 super(props);
1229 this.state = {x: this.props.x + '(ctor)'};
1230 }
1231 UNSAFE_componentWillMount() {
1232 Scheduler.log('componentWillMount:' + this.state.x);
1233 this.setState({x: this.props.x + '(willMount)'});
1234 }
1235 componentDidMount() {
1236 Scheduler.log('componentDidMount:' + this.state.x);
1237 }
1238 render() {
1239 Scheduler.log('render:' + this.state.x);
1240 return <span />;
1241 }
1242 }
1243
1244 function App(props) {
1245 Scheduler.log('App');
1246 return <LifeCycle x={props.x} />;
1247 }
1248
1249 ReactNoop.render(<App x={0} />);
1250 ReactNoop.flushDeferredPri(20);
1251
1252 assertLog(['App', 'componentWillMount:0(ctor)', 'render:0(willMount)']);
1253
1254 ReactNoop.render(<App x={1} />);
1255 await waitForAll([
1256 'App',
1257 'componentWillMount:0(willMount)',
1258 'render:1(willMount)',
1259 'componentDidMount:1(willMount)',
1260 ]);
1261 });
1262
1263 // eslint-disable-next-line jest/no-disabled-tests
1264 it.skip('calls componentWill* twice if an update render is aborted', async () => {
1265 class LifeCycle extends React.Component {
1266 UNSAFE_componentWillMount() {
1267 Scheduler.log('componentWillMount:' + this.props.x);
1268 }
1269 componentDidMount() {
1270 Scheduler.log('componentDidMount:' + this.props.x);
1271 }
1272 UNSAFE_componentWillReceiveProps(nextProps) {
1273 Scheduler.log(
1274 'componentWillReceiveProps:' + this.props.x + '-' + nextProps.x,
1275 );
1276 }
1277 shouldComponentUpdate(nextProps) {
1278 Scheduler.log(
1279 'shouldComponentUpdate:' + this.props.x + '-' + nextProps.x,
1280 );
1281 return true;
1282 }
1283 UNSAFE_componentWillUpdate(nextProps) {
1284 Scheduler.log(
1285 'componentWillUpdate:' + this.props.x + '-' + nextProps.x,
1286 );
1287 }
1288 componentDidUpdate(prevProps) {
1289 Scheduler.log('componentDidUpdate:' + this.props.x + '-' + prevProps.x);
1290 }
1291 render() {
1292 Scheduler.log('render:' + this.props.x);
1293 return <span />;
1294 }
1295 }
1296
1297 function Sibling() {
1298 // The sibling is used to confirm that we've completed the first child,
1299 // but not yet flushed.
1300 Scheduler.log('Sibling');
1301 return <span />;
1302 }
1303
1304 function App(props) {
1305 Scheduler.log('App');
1306
1307 return [<LifeCycle key="a" x={props.x} />, <Sibling key="b" />];
1308 }
1309
1310 ReactNoop.render(<App x={0} />);
1311 await waitForAll([
1312 'App',
1313 'componentWillMount:0',
1314 'render:0',
1315 'Sibling',
1316 'componentDidMount:0',
1317 ]);
1318
1319 ReactNoop.render(<App x={1} />);
1320 ReactNoop.flushDeferredPri(30);
1321
1322 assertLog([
1323 'App',
1324 'componentWillReceiveProps:0-1',
1325 'shouldComponentUpdate:0-1',
1326 'componentWillUpdate:0-1',
1327 'render:1',
1328 'Sibling',
1329 // no componentDidUpdate
1330 ]);
1331
1332 ReactNoop.render(<App x={2} />);
1333 await waitForAll([
1334 'App',
1335 'componentWillReceiveProps:1-2',
1336 'shouldComponentUpdate:1-2',
1337 'componentWillUpdate:1-2',
1338 'render:2',
1339 'Sibling',
1340 // When componentDidUpdate finally gets called, it covers both updates.
1341 'componentDidUpdate:2-0',
1342 ]);
1343 });
1344
1345 it('calls getDerivedStateFromProps even for state-only updates', async () => {
1346 let instance;
1347
1348 class LifeCycle extends React.Component {
1349 state = {};
1350 static getDerivedStateFromProps(props, prevState) {
1351 Scheduler.log('getDerivedStateFromProps');
1352 return {foo: 'foo'};
1353 }
1354 changeState() {
1355 this.setState({foo: 'bar'});
1356 }
1357 componentDidUpdate() {
1358 Scheduler.log('componentDidUpdate');
1359 }
1360 render() {
1361 Scheduler.log('render');
1362 instance = this;
1363 return null;
1364 }
1365 }
1366
1367 ReactNoop.render(<LifeCycle />);
1368 await waitForAll(['getDerivedStateFromProps', 'render']);
1369 expect(instance.state).toEqual({foo: 'foo'});
1370
1371 instance.changeState();
1372 await waitForAll([
1373 'getDerivedStateFromProps',
1374 'render',
1375 'componentDidUpdate',
1376 ]);
1377 expect(instance.state).toEqual({foo: 'foo'});
1378 });
1379
1380 it('does not call getDerivedStateFromProps if neither state nor props have changed', async () => {
1381 class Parent extends React.Component {
1382 state = {parentRenders: 0};
1383 static getDerivedStateFromProps(props, prevState) {
1384 Scheduler.log('getDerivedStateFromProps');
1385 return prevState.parentRenders + 1;
1386 }
1387 render() {
1388 Scheduler.log('Parent');
1389 return <Child parentRenders={this.state.parentRenders} ref={child} />;
1390 }
1391 }
1392
1393 class Child extends React.Component {
1394 render() {
1395 Scheduler.log('Child');
1396 return this.props.parentRenders;
1397 }
1398 }
1399
1400 const child = React.createRef(null);
1401 ReactNoop.render(<Parent />);
1402 await waitForAll(['getDerivedStateFromProps', 'Parent', 'Child']);
1403
1404 // Schedule an update on the child. The parent should not re-render.
1405 child.current.setState({});
1406 await waitForAll(['Child']);
1407 });
1408
1409 // eslint-disable-next-line jest/no-disabled-tests
1410 it.skip('does not call componentWillReceiveProps for state-only updates', async () => {
1411 const instances = [];
1412
1413 class LifeCycle extends React.Component {
1414 state = {x: 0};
1415 tick() {
1416 this.setState({
1417 x: this.state.x + 1,
1418 });
1419 }
1420 UNSAFE_componentWillMount() {
1421 instances.push(this);
1422 Scheduler.log('componentWillMount:' + this.state.x);
1423 }
1424 componentDidMount() {
1425 Scheduler.log('componentDidMount:' + this.state.x);
1426 }
1427 UNSAFE_componentWillReceiveProps(nextProps) {
1428 Scheduler.log('componentWillReceiveProps');
1429 }
1430 shouldComponentUpdate(nextProps, nextState) {
1431 Scheduler.log(
1432 'shouldComponentUpdate:' + this.state.x + '-' + nextState.x,
1433 );
1434 return true;
1435 }
1436 UNSAFE_componentWillUpdate(nextProps, nextState) {
1437 Scheduler.log(
1438 'componentWillUpdate:' + this.state.x + '-' + nextState.x,
1439 );
1440 }
1441 componentDidUpdate(prevProps, prevState) {
1442 Scheduler.log('componentDidUpdate:' + this.state.x + '-' + prevState.x);
1443 }
1444 render() {
1445 Scheduler.log('render:' + this.state.x);
1446 return <span />;
1447 }
1448 }
1449
1450 // This wrap is a bit contrived because we can't pause a completed root and
1451 // there is currently an issue where a component can't reuse its render
1452 // output unless it fully completed.
1453 class Wrap extends React.Component {
1454 state = {y: 0};
1455 UNSAFE_componentWillMount() {
1456 instances.push(this);
1457 }
1458 tick() {
1459 this.setState({
1460 y: this.state.y + 1,
1461 });
1462 }
1463 render() {
1464 Scheduler.log('Wrap');
1465 return <LifeCycle y={this.state.y} />;
1466 }
1467 }
1468
1469 function Sibling() {
1470 // The sibling is used to confirm that we've completed the first child,
1471 // but not yet flushed.
1472 Scheduler.log('Sibling');
1473 return <span />;
1474 }
1475
1476 function App(props) {
1477 Scheduler.log('App');
1478 return [<Wrap key="a" />, <Sibling key="b" />];
1479 }
1480
1481 ReactNoop.render(<App y={0} />);
1482 await waitForAll([
1483 'App',
1484 'Wrap',
1485 'componentWillMount:0',
1486 'render:0',
1487 'Sibling',
1488 'componentDidMount:0',
1489 ]);
1490
1491 // LifeCycle
1492 instances[1].tick();
1493
1494 ReactNoop.flushDeferredPri(25);
1495
1496 assertLog([
1497 // no componentWillReceiveProps
1498 'shouldComponentUpdate:0-1',
1499 'componentWillUpdate:0-1',
1500 'render:1',
1501 // no componentDidUpdate
1502 ]);
1503
1504 // LifeCycle
1505 instances[1].tick();
1506
1507 await waitForAll([
1508 // no componentWillReceiveProps
1509 'shouldComponentUpdate:1-2',
1510 'componentWillUpdate:1-2',
1511 'render:2',
1512 // When componentDidUpdate finally gets called, it covers both updates.
1513 'componentDidUpdate:2-0',
1514 ]);
1515
1516 // Next we will update props of LifeCycle by updating its parent.
1517
1518 instances[0].tick();
1519
1520 ReactNoop.flushDeferredPri(30);
1521
1522 assertLog([
1523 'Wrap',
1524 'componentWillReceiveProps',
1525 'shouldComponentUpdate:2-2',
1526 'componentWillUpdate:2-2',
1527 'render:2',
1528 // no componentDidUpdate
1529 ]);
1530
1531 // Next we will update LifeCycle directly but not with new props.
1532 instances[1].tick();
1533
1534 await waitForAll([
1535 // This should not trigger another componentWillReceiveProps because
1536 // we never got new props.
1537 'shouldComponentUpdate:2-3',
1538 'componentWillUpdate:2-3',
1539 'render:3',
1540 'componentDidUpdate:3-2',
1541 ]);
1542
1543 // TODO: Test that we get the expected values for the same scenario with
1544 // incomplete parents.
1545 });
1546
1547 // eslint-disable-next-line jest/no-disabled-tests
1548 it.skip('skips will/DidUpdate when bailing unless an update was already in progress', async () => {
1549 class LifeCycle extends React.Component {
1550 UNSAFE_componentWillMount() {
1551 Scheduler.log('componentWillMount');
1552 }
1553 componentDidMount() {
1554 Scheduler.log('componentDidMount');
1555 }
1556 UNSAFE_componentWillReceiveProps(nextProps) {
1557 Scheduler.log('componentWillReceiveProps');
1558 }
1559 shouldComponentUpdate(nextProps) {
1560 Scheduler.log('shouldComponentUpdate');
1561 // Bail
1562 return this.props.x !== nextProps.x;
1563 }
1564 UNSAFE_componentWillUpdate(nextProps) {
1565 Scheduler.log('componentWillUpdate');
1566 }
1567 componentDidUpdate(prevProps) {
1568 Scheduler.log('componentDidUpdate');
1569 }
1570 render() {
1571 Scheduler.log('render');
1572 return <span />;
1573 }
1574 }
1575
1576 function Sibling() {
1577 Scheduler.log('render sibling');
1578 return <span />;
1579 }
1580
1581 function App(props) {
1582 return [<LifeCycle key="a" x={props.x} />, <Sibling key="b" />];
1583 }
1584
1585 ReactNoop.render(<App x={0} />);
1586 await waitForAll([
1587 'componentWillMount',
1588 'render',
1589 'render sibling',
1590 'componentDidMount',
1591 ]);
1592
1593 // Update to same props
1594 ReactNoop.render(<App x={0} />);
1595 await waitForAll([
1596 'componentWillReceiveProps',
1597 'shouldComponentUpdate',
1598 // no componentWillUpdate
1599 // no render
1600 'render sibling',
1601 // no componentDidUpdate
1602 ]);
1603
1604 // Begin updating to new props...
1605 ReactNoop.render(<App x={1} />);
1606 ReactNoop.flushDeferredPri(30);
1607
1608 assertLog([
1609 'componentWillReceiveProps',
1610 'shouldComponentUpdate',
1611 'componentWillUpdate',
1612 'render',
1613 'render sibling',
1614 // no componentDidUpdate yet
1615 ]);
1616
1617 // ...but we'll interrupt it to rerender the same props.
1618 ReactNoop.render(<App x={1} />);
1619 await waitForAll([]);
1620
1621 // We can bail out this time, but we must call componentDidUpdate.
1622 assertLog([
1623 'componentWillReceiveProps',
1624 'shouldComponentUpdate',
1625 // no componentWillUpdate
1626 // no render
1627 'render sibling',
1628 'componentDidUpdate',
1629 ]);
1630 });
1631
1632 it('can nest batchedUpdates', async () => {
1633 let instance;
1634
1635 class Foo extends React.Component {
1636 state = {n: 0};
1637 render() {
1638 instance = this;
1639 return <div />;
1640 }
1641 }
1642
1643 ReactNoop.render(<Foo />);
1644 await waitForAll([]);
1645
1646 ReactNoop.flushSync(() => {
1647 ReactNoop.batchedUpdates(() => {
1648 instance.setState({n: 1}, () => Scheduler.log('setState 1'));
1649 instance.setState({n: 2}, () => Scheduler.log('setState 2'));
1650 ReactNoop.batchedUpdates(() => {
1651 instance.setState({n: 3}, () => Scheduler.log('setState 3'));
1652 instance.setState({n: 4}, () => Scheduler.log('setState 4'));
1653 Scheduler.log('end inner batchedUpdates');
1654 });
1655 Scheduler.log('end outer batchedUpdates');
1656 });
1657 });
1658
1659 // ReactNoop.flush() not needed because updates are synchronous
1660
1661 assertLog([
1662 'end inner batchedUpdates',
1663 'end outer batchedUpdates',
1664 'setState 1',
1665 'setState 2',
1666 'setState 3',
1667 'setState 4',
1668 ]);
1669 expect(instance.state.n).toEqual(4);
1670 });
1671
1672 it('can handle if setState callback throws', async () => {
1673 let instance;
1674
1675 class Foo extends React.Component {
1676 state = {n: 0};
1677 render() {
1678 instance = this;
1679 return <div />;
1680 }
1681 }
1682
1683 ReactNoop.render(<Foo />);
1684 await waitForAll([]);
1685
1686 function updater({n}) {
1687 return {n: n + 1};
1688 }
1689
1690 instance.setState(updater, () => Scheduler.log('first callback'));
1691 instance.setState(updater, () => {
1692 Scheduler.log('second callback');
1693 throw new Error('callback error');
1694 });
1695 instance.setState(updater, () => Scheduler.log('third callback'));
1696
1697 await waitForThrow('callback error');
1698
1699 // The third callback isn't called because the second one throws
1700 assertLog(['first callback', 'second callback']);
1701 expect(instance.state.n).toEqual(3);
1702 });
1703
1704 // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1705 it('merges and masks context', async () => {
1706 class Intl extends React.Component {
1707 static childContextTypes = {
1708 locale: PropTypes.string,
1709 };
1710 getChildContext() {
1711 return {
1712 locale: this.props.locale,
1713 };
1714 }
1715 render() {
1716 Scheduler.log('Intl ' + JSON.stringify(this.context));
1717 return this.props.children;
1718 }
1719 }
1720
1721 class Router extends React.Component {
1722 static childContextTypes = {
1723 route: PropTypes.string,
1724 };
1725 getChildContext() {
1726 return {
1727 route: this.props.route,
1728 };
1729 }
1730 render() {
1731 Scheduler.log('Router ' + JSON.stringify(this.context));
1732 return this.props.children;
1733 }
1734 }
1735
1736 class ShowLocale extends React.Component {
1737 static contextTypes = {
1738 locale: PropTypes.string,
1739 };
1740 render() {
1741 Scheduler.log('ShowLocale ' + JSON.stringify(this.context));
1742 return this.context.locale;
1743 }
1744 }
1745
1746 class ShowRoute extends React.Component {
1747 static contextTypes = {
1748 route: PropTypes.string,
1749 };
1750 render() {
1751 Scheduler.log('ShowRoute ' + JSON.stringify(this.context));
1752 return this.context.route;
1753 }
1754 }
1755
1756 function ShowBoth(props, context) {
1757 Scheduler.log('ShowBoth ' + JSON.stringify(context));
1758 return `${context.route} in ${context.locale}`;
1759 }
1760 ShowBoth.contextTypes = {
1761 locale: PropTypes.string,
1762 route: PropTypes.string,
1763 };
1764
1765 class ShowNeither extends React.Component {
1766 render() {
1767 Scheduler.log('ShowNeither ' + JSON.stringify(this.context));
1768 return null;
1769 }
1770 }
1771
1772 class Indirection extends React.Component {
1773 render() {
1774 Scheduler.log('Indirection ' + JSON.stringify(this.context));
1775 return [
1776 <ShowLocale key="a" />,
1777 <ShowRoute key="b" />,
1778 <ShowNeither key="c" />,
1779 <Intl key="d" locale="ru">
1780 <ShowBoth />
1781 </Intl>,
1782 <ShowBoth key="e" />,
1783 ];
1784 }
1785 }
1786
1787 ReactNoop.render(
1788 <Intl locale="fr">
1789 <ShowLocale />
1790 <div>
1791 <ShowBoth />
1792 </div>
1793 </Intl>,
1794 );
1795 await waitForAll([
1796 'Intl {}',
1797 'ShowLocale {"locale":"fr"}',
1798 'ShowBoth {"locale":"fr"}',
1799 ]);
1800 assertConsoleErrorDev([
1801 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
1802 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1803 ' in Intl (at **)',
1804 'ShowLocale uses the legacy contextTypes API which will soon be removed. ' +
1805 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1806 ' in ShowLocale (at **)',
1807 'ShowBoth uses the legacy contextTypes API which will be removed soon. ' +
1808 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
1809 ' in ShowBoth (at **)',
1810 ]);
1811
1812 ReactNoop.render(
1813 <Intl locale="de">
1814 <ShowLocale />
1815 <div>
1816 <ShowBoth />
1817 </div>
1818 </Intl>,
1819 );
1820 await waitForAll([
1821 'Intl {}',
1822 'ShowLocale {"locale":"de"}',
1823 'ShowBoth {"locale":"de"}',
1824 ]);
1825 React.startTransition(() => {
1826 ReactNoop.render(
1827 <Intl locale="sv">
1828 <ShowLocale />
1829 <div>
1830 <ShowBoth />
1831 </div>
1832 </Intl>,
1833 );
1834 });
1835 await waitFor(['Intl {}']);
1836
1837 ReactNoop.render(
1838 <Intl locale="en">
1839 <ShowLocale />
1840 <Router route="/about">
1841 <Indirection />
1842 </Router>
1843 <ShowBoth />
1844 </Intl>,
1845 );
1846 await waitForAll([
1847 'ShowLocale {"locale":"sv"}',
1848 'ShowBoth {"locale":"sv"}',
1849 'Intl {}',
1850 'ShowLocale {"locale":"en"}',
1851 'Router {}',
1852 'Indirection {}',
1853 'ShowLocale {"locale":"en"}',
1854 'ShowRoute {"route":"/about"}',
1855 'ShowNeither {}',
1856 'Intl {}',
1857 'ShowBoth {"locale":"ru","route":"/about"}',
1858 'ShowBoth {"locale":"en","route":"/about"}',
1859 'ShowBoth {"locale":"en"}',
1860 ]);
1861 assertConsoleErrorDev([
1862 'Router uses the legacy childContextTypes API which will soon be removed. ' +
1863 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1864 ' in Router (at **)',
1865 'ShowRoute uses the legacy contextTypes API which will soon be removed. ' +
1866 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1867 ' in Indirection (at **)',
1868 ]);
1869 });
1870
1871 // @gate !disableLegacyContext
1872 it('does not leak own context into context provider', async () => {
1873 if (gate(flags => flags.disableLegacyContext)) {
1874 throw new Error('This test infinite loops when context is disabled.');
1875 }
1876 class Recurse extends React.Component {
1877 static contextTypes = {
1878 n: PropTypes.number,
1879 };
1880 static childContextTypes = {
1881 n: PropTypes.number,
1882 };
1883 getChildContext() {
1884 return {n: (this.context.n || 3) - 1};
1885 }
1886 render() {
1887 Scheduler.log('Recurse ' + JSON.stringify(this.context));
1888 if (this.context.n === 0) {
1889 return null;
1890 }
1891 return <Recurse />;
1892 }
1893 }
1894
1895 ReactNoop.render(<Recurse />);
1896 await waitForAll([
1897 'Recurse {}',
1898 'Recurse {"n":2}',
1899 'Recurse {"n":1}',
1900 'Recurse {"n":0}',
1901 ]);
1902 assertConsoleErrorDev([
1903 'Recurse uses the legacy childContextTypes API which will soon be removed. ' +
1904 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1905 ' in Recurse (at **)',
1906 'Recurse uses the legacy contextTypes API which will soon be removed. ' +
1907 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1908 ' in Recurse (at **)',
1909 ]);
1910 });
1911
1912 // @gate enableLegacyHidden && !disableLegacyContext
1913 it('provides context when reusing work', async () => {
1914 class Intl extends React.Component {
1915 static childContextTypes = {
1916 locale: PropTypes.string,
1917 };
1918 getChildContext() {
1919 return {
1920 locale: this.props.locale,
1921 };
1922 }
1923 render() {
1924 Scheduler.log('Intl ' + JSON.stringify(this.context));
1925 return this.props.children;
1926 }
1927 }
1928
1929 class ShowLocale extends React.Component {
1930 static contextTypes = {
1931 locale: PropTypes.string,
1932 };
1933 render() {
1934 Scheduler.log('ShowLocale ' + JSON.stringify(this.context));
1935 return this.context.locale;
1936 }
1937 }
1938
1939 React.startTransition(() => {
1940 ReactNoop.render(
1941 <Intl locale="fr">
1942 <ShowLocale />
1943 <LegacyHiddenDiv mode="hidden">
1944 <ShowLocale />
1945 <Intl locale="ru">
1946 <ShowLocale />
1947 </Intl>
1948 </LegacyHiddenDiv>
1949 <ShowLocale />
1950 </Intl>,
1951 );
1952 });
1953
1954 await waitFor([
1955 'Intl {}',
1956 'ShowLocale {"locale":"fr"}',
1957 'ShowLocale {"locale":"fr"}',
1958 ]);
1959 assertConsoleErrorDev([
1960 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
1961 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1962 ' in Intl (at **)',
1963 'ShowLocale uses the legacy contextTypes API which will soon be removed. ' +
1964 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
1965 ' in ShowLocale (at **)',
1966 ]);
1967
1968 await waitForAll([
1969 'ShowLocale {"locale":"fr"}',
1970 'Intl {}',
1971 'ShowLocale {"locale":"ru"}',
1972 ]);
1973 });
1974
1975 // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
1976 it('reads context when setState is below the provider', async () => {
1977 let statefulInst;
1978
1979 class Intl extends React.Component {
1980 static childContextTypes = {
1981 locale: PropTypes.string,
1982 };
1983 getChildContext() {
1984 const childContext = {
1985 locale: this.props.locale,
1986 };
1987 Scheduler.log('Intl:provide ' + JSON.stringify(childContext));
1988 return childContext;
1989 }
1990 render() {
1991 Scheduler.log('Intl:read ' + JSON.stringify(this.context));
1992 return this.props.children;
1993 }
1994 }
1995
1996 class ShowLocaleClass extends React.Component {
1997 static contextTypes = {
1998 locale: PropTypes.string,
1999 };
2000 render() {
2001 Scheduler.log('ShowLocaleClass:read ' + JSON.stringify(this.context));
2002 return this.context.locale;
2003 }
2004 }
2005
2006 function ShowLocaleFn(props, context) {
2007 Scheduler.log('ShowLocaleFn:read ' + JSON.stringify(context));
2008 return context.locale;
2009 }
2010 ShowLocaleFn.contextTypes = {
2011 locale: PropTypes.string,
2012 };
2013
2014 class Stateful extends React.Component {
2015 state = {x: 0};
2016 render() {
2017 statefulInst = this;
2018 return this.props.children;
2019 }
2020 }
2021
2022 function IndirectionFn(props, context) {
2023 Scheduler.log('IndirectionFn ' + JSON.stringify(context));
2024 return props.children;
2025 }
2026
2027 class IndirectionClass extends React.Component {
2028 render() {
2029 Scheduler.log('IndirectionClass ' + JSON.stringify(this.context));
2030 return this.props.children;
2031 }
2032 }
2033
2034 ReactNoop.render(
2035 <Intl locale="fr">
2036 <IndirectionFn>
2037 <IndirectionClass>
2038 <Stateful>
2039 <ShowLocaleClass />
2040 <ShowLocaleFn />
2041 </Stateful>
2042 </IndirectionClass>
2043 </IndirectionFn>
2044 </Intl>,
2045 );
2046 await waitForAll([
2047 'Intl:read {}',
2048 'Intl:provide {"locale":"fr"}',
2049 'IndirectionFn {}',
2050 'IndirectionClass {}',
2051 'ShowLocaleClass:read {"locale":"fr"}',
2052 'ShowLocaleFn:read {"locale":"fr"}',
2053 ]);
2054 assertConsoleErrorDev([
2055 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
2056 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2057 ' in Intl (at **)',
2058 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. ' +
2059 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2060 ' in ShowLocaleClass (at **)',
2061 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. ' +
2062 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
2063 ' in ShowLocaleFn (at **)',
2064 ]);
2065
2066 statefulInst.setState({x: 1});
2067 await waitForAll([]);
2068 // All work has been memoized because setState()
2069 // happened below the context and could not have affected it.
2070 assertLog([]);
2071 });
2072
2073 // @gate !disableLegacyContext && !disableLegacyContextForFunctionComponents
2074 it('reads context when setState is above the provider', async () => {
2075 let statefulInst;
2076
2077 class Intl extends React.Component {
2078 static childContextTypes = {
2079 locale: PropTypes.string,
2080 };
2081 getChildContext() {
2082 const childContext = {
2083 locale: this.props.locale,
2084 };
2085 Scheduler.log('Intl:provide ' + JSON.stringify(childContext));
2086 return childContext;
2087 }
2088 render() {
2089 Scheduler.log('Intl:read ' + JSON.stringify(this.context));
2090 return this.props.children;
2091 }
2092 }
2093
2094 class ShowLocaleClass extends React.Component {
2095 static contextTypes = {
2096 locale: PropTypes.string,
2097 };
2098 render() {
2099 Scheduler.log('ShowLocaleClass:read ' + JSON.stringify(this.context));
2100 return this.context.locale;
2101 }
2102 }
2103
2104 function ShowLocaleFn(props, context) {
2105 Scheduler.log('ShowLocaleFn:read ' + JSON.stringify(context));
2106 return context.locale;
2107 }
2108 ShowLocaleFn.contextTypes = {
2109 locale: PropTypes.string,
2110 };
2111
2112 function IndirectionFn(props, context) {
2113 Scheduler.log('IndirectionFn ' + JSON.stringify(context));
2114 return props.children;
2115 }
2116
2117 class IndirectionClass extends React.Component {
2118 render() {
2119 Scheduler.log('IndirectionClass ' + JSON.stringify(this.context));
2120 return this.props.children;
2121 }
2122 }
2123
2124 class Stateful extends React.Component {
2125 state = {locale: 'fr'};
2126 render() {
2127 statefulInst = this;
2128 return <Intl locale={this.state.locale}>{this.props.children}</Intl>;
2129 }
2130 }
2131
2132 ReactNoop.render(
2133 <Stateful>
2134 <IndirectionFn>
2135 <IndirectionClass>
2136 <ShowLocaleClass />
2137 <ShowLocaleFn />
2138 </IndirectionClass>
2139 </IndirectionFn>
2140 </Stateful>,
2141 );
2142 await waitForAll([
2143 'Intl:read {}',
2144 'Intl:provide {"locale":"fr"}',
2145 'IndirectionFn {}',
2146 'IndirectionClass {}',
2147 'ShowLocaleClass:read {"locale":"fr"}',
2148 'ShowLocaleFn:read {"locale":"fr"}',
2149 ]);
2150
2151 assertConsoleErrorDev([
2152 'Intl uses the legacy childContextTypes API which will soon be removed. ' +
2153 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2154 ' in Stateful (at **)',
2155 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. ' +
2156 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2157 ' in ShowLocaleClass (at **)',
2158
2159 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. ' +
2160 'Use React.createContext() with React.useContext() instead. (https://react.dev/link/legacy-context)\n' +
2161 ' in ShowLocaleFn (at **)',
2162 ]);
2163
2164 statefulInst.setState({locale: 'gr'});
2165 await waitForAll([
2166 // Intl is below setState() so it might have been
2167 // affected by it. Therefore we re-render and recompute
2168 // its child context.
2169 'Intl:read {}',
2170 'Intl:provide {"locale":"gr"}',
2171 // TODO: it's unfortunate that we can't reuse work on
2172 // these components even though they don't depend on context.
2173 'IndirectionFn {}',
2174 'IndirectionClass {}',
2175 // These components depend on context:
2176 'ShowLocaleClass:read {"locale":"gr"}',
2177 'ShowLocaleFn:read {"locale":"gr"}',
2178 ]);
2179 });
2180
2181 // @gate !disableLegacyContext || !__DEV__
2182 it('maintains the correct context when providers bail out due to low priority', async () => {
2183 class Root extends React.Component {
2184 render() {
2185 return <Middle {...this.props} />;
2186 }
2187 }
2188
2189 let instance;
2190
2191 class Middle extends React.Component {
2192 constructor(props, context) {
2193 super(props, context);
2194 instance = this;
2195 }
2196 shouldComponentUpdate() {
2197 // Return false so that our child will get a NoWork priority (and get bailed out)
2198 return false;
2199 }
2200 render() {
2201 return <Child />;
2202 }
2203 }
2204
2205 // Child must be a context provider to trigger the bug
2206 class Child extends React.Component {
2207 static childContextTypes = {};
2208 getChildContext() {
2209 return {};
2210 }
2211 render() {
2212 return <div />;
2213 }
2214 }
2215
2216 // Init
2217 ReactNoop.render(<Root />);
2218 await waitForAll([]);
2219
2220 assertConsoleErrorDev([
2221 'Child uses the legacy childContextTypes API which will soon be removed. ' +
2222 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2223 ' in Middle (at **)\n' +
2224 ' in Root (at **)',
2225 ]);
2226
2227 // Trigger an update in the middle of the tree
2228 instance.setState({});
2229 await waitForAll([]);
2230 });
2231
2232 // @gate !disableLegacyContext || !__DEV__
2233 it('maintains the correct context when unwinding due to an error in render', async () => {
2234 class Root extends React.Component {
2235 componentDidCatch(error) {
2236 // If context is pushed/popped correctly,
2237 // This method will be used to handle the intentionally-thrown Error.
2238 }
2239 render() {
2240 return <ContextProvider depth={1} />;
2241 }
2242 }
2243
2244 let instance;
2245
2246 class ContextProvider extends React.Component {
2247 constructor(props, context) {
2248 super(props, context);
2249 this.state = {};
2250 if (props.depth === 1) {
2251 instance = this;
2252 }
2253 }
2254 static childContextTypes = {};
2255 getChildContext() {
2256 return {};
2257 }
2258 render() {
2259 if (this.state.throwError) {
2260 throw Error();
2261 }
2262 return this.props.depth < 4 ? (
2263 <ContextProvider depth={this.props.depth + 1} />
2264 ) : (
2265 <div />
2266 );
2267 }
2268 }
2269
2270 // Init
2271 ReactNoop.render(<Root />);
2272 await waitForAll([]);
2273 assertConsoleErrorDev([
2274 'ContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2275 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2276 ' in Root (at **)',
2277 ]);
2278
2279 // Trigger an update in the middle of the tree
2280 // This is necessary to reproduce the error as it currently exists.
2281 instance.setState({
2282 throwError: true,
2283 });
2284 await waitForAll([]);
2285 assertConsoleErrorDev([
2286 'Root: Error boundaries should implement getDerivedStateFromError(). ' +
2287 'In that method, return a state update to display an error message or fallback UI.\n' +
2288 ' in Root (at **)',
2289 ]);
2290 });
2291
2292 // @gate !disableLegacyContext || !__DEV__
2293 it('should not recreate masked context unless inputs have changed', async () => {
2294 let scuCounter = 0;
2295
2296 class MyComponent extends React.Component {
2297 static contextTypes = {};
2298 componentDidMount(prevProps, prevState) {
2299 Scheduler.log('componentDidMount');
2300 this.setState({setStateInCDU: true});
2301 }
2302 componentDidUpdate(prevProps, prevState) {
2303 Scheduler.log('componentDidUpdate');
2304 if (this.state.setStateInCDU) {
2305 this.setState({setStateInCDU: false});
2306 }
2307 }
2308 UNSAFE_componentWillReceiveProps(nextProps) {
2309 Scheduler.log('componentWillReceiveProps');
2310 this.setState({setStateInCDU: true});
2311 }
2312 render() {
2313 Scheduler.log('render');
2314 return null;
2315 }
2316 shouldComponentUpdate(nextProps, nextState) {
2317 Scheduler.log('shouldComponentUpdate');
2318 return scuCounter++ < 5; // Don't let test hang
2319 }
2320 }
2321
2322 ReactNoop.render(<MyComponent />);
2323 await waitForAll([
2324 'render',
2325 'componentDidMount',
2326 'shouldComponentUpdate',
2327 'render',
2328 'componentDidUpdate',
2329 'shouldComponentUpdate',
2330 'render',
2331 'componentDidUpdate',
2332 ]);
2333
2334 assertConsoleErrorDev([
2335 'MyComponent uses the legacy contextTypes API which will soon be removed. ' +
2336 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2337 ' in MyComponent (at **)',
2338 ]);
2339 });
2340
2341 // eslint-disable-next-line jest/no-disabled-tests
2342 it.skip('should reuse memoized work if pointers are updated before calling lifecycles', async () => {
2343 const cduNextProps = [];
2344 const cduPrevProps = [];
2345 const scuNextProps = [];
2346 const scuPrevProps = [];
2347 let renderCounter = 0;
2348
2349 function SecondChild(props) {
2350 return <span>{props.children}</span>;
2351 }
2352
2353 class FirstChild extends React.Component {
2354 componentDidUpdate(prevProps, prevState) {
2355 cduNextProps.push(this.props);
2356 cduPrevProps.push(prevProps);
2357 }
2358 shouldComponentUpdate(nextProps, nextState) {
2359 scuNextProps.push(nextProps);
2360 scuPrevProps.push(this.props);
2361 return this.props.children !== nextProps.children;
2362 }
2363 render() {
2364 renderCounter++;
2365 return <span>{this.props.children}</span>;
2366 }
2367 }
2368
2369 class Middle extends React.Component {
2370 render() {
2371 return (
2372 <div>
2373 <FirstChild>{this.props.children}</FirstChild>
2374 <SecondChild>{this.props.children}</SecondChild>
2375 </div>
2376 );
2377 }
2378 }
2379
2380 function Root(props) {
2381 return (
2382 <div hidden={true}>
2383 <Middle {...props} />
2384 </div>
2385 );
2386 }
2387
2388 // Initial render of the entire tree.
2389 // Renders: Root, Middle, FirstChild, SecondChild
2390 ReactNoop.render(<Root>A</Root>);
2391 await waitForAll([]);
2392
2393 expect(renderCounter).toBe(1);
2394
2395 // Schedule low priority work to update children.
2396 // Give it enough time to partially render.
2397 // Renders: Root, Middle, FirstChild
2398 ReactNoop.render(<Root>B</Root>);
2399 ReactNoop.flushDeferredPri(20 + 30 + 5);
2400
2401 // At this point our FirstChild component has rendered a second time,
2402 // But since the render is not completed cDU should not be called yet.
2403 expect(renderCounter).toBe(2);
2404 expect(scuPrevProps).toEqual([{children: 'A'}]);
2405 expect(scuNextProps).toEqual([{children: 'B'}]);
2406 expect(cduPrevProps).toEqual([]);
2407 expect(cduNextProps).toEqual([]);
2408
2409 // Next interrupt the partial render with higher priority work.
2410 // The in-progress child content will bailout.
2411 // Renders: Root, Middle, FirstChild, SecondChild
2412 ReactNoop.render(<Root>B</Root>);
2413 await waitForAll([]);
2414
2415 // At this point the higher priority render has completed.
2416 // Since FirstChild props didn't change, sCU returned false.
2417 // The previous memoized copy should be used.
2418 expect(renderCounter).toBe(2);
2419 expect(scuPrevProps).toEqual([{children: 'A'}, {children: 'B'}]);
2420 expect(scuNextProps).toEqual([{children: 'B'}, {children: 'B'}]);
2421 expect(cduPrevProps).toEqual([{children: 'A'}]);
2422 expect(cduNextProps).toEqual([{children: 'B'}]);
2423 });
2424
2425 // @gate !disableLegacyContext
2426 it('updates descendants with new context values', async () => {
2427 let instance;
2428
2429 class TopContextProvider extends React.Component {
2430 static childContextTypes = {
2431 count: PropTypes.number,
2432 };
2433 constructor() {
2434 super();
2435 this.state = {count: 0};
2436 instance = this;
2437 }
2438 getChildContext = () => ({
2439 count: this.state.count,
2440 });
2441 render = () => this.props.children;
2442 updateCount = () =>
2443 this.setState(state => ({
2444 count: state.count + 1,
2445 }));
2446 }
2447
2448 class Middle extends React.Component {
2449 render = () => this.props.children;
2450 }
2451
2452 class Child extends React.Component {
2453 static contextTypes = {
2454 count: PropTypes.number,
2455 };
2456 render = () => {
2457 Scheduler.log(`count:${this.context.count}`);
2458 return null;
2459 };
2460 }
2461
2462 ReactNoop.render(
2463 <TopContextProvider>
2464 <Middle>
2465 <Child />
2466 </Middle>
2467 </TopContextProvider>,
2468 );
2469
2470 await waitForAll(['count:0']);
2471 assertConsoleErrorDev([
2472 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2473 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2474 ' in TopContextProvider (at **)',
2475 'Child uses the legacy contextTypes API which will soon be removed. ' +
2476 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2477 ' in Child (at **)',
2478 ]);
2479 instance.updateCount();
2480 await waitForAll(['count:1']);
2481 });
2482
2483 // @gate !disableLegacyContext
2484 it('updates descendants with multiple context-providing ancestors with new context values', async () => {
2485 let instance;
2486
2487 class TopContextProvider extends React.Component {
2488 static childContextTypes = {
2489 count: PropTypes.number,
2490 };
2491 constructor() {
2492 super();
2493 this.state = {count: 0};
2494 instance = this;
2495 }
2496 getChildContext = () => ({
2497 count: this.state.count,
2498 });
2499 render = () => this.props.children;
2500 updateCount = () =>
2501 this.setState(state => ({
2502 count: state.count + 1,
2503 }));
2504 }
2505
2506 class MiddleContextProvider extends React.Component {
2507 static childContextTypes = {
2508 name: PropTypes.string,
2509 };
2510 getChildContext = () => ({
2511 name: 'brian',
2512 });
2513 render = () => this.props.children;
2514 }
2515
2516 class Child extends React.Component {
2517 static contextTypes = {
2518 count: PropTypes.number,
2519 };
2520 render = () => {
2521 Scheduler.log(`count:${this.context.count}`);
2522 return null;
2523 };
2524 }
2525
2526 ReactNoop.render(
2527 <TopContextProvider>
2528 <MiddleContextProvider>
2529 <Child />
2530 </MiddleContextProvider>
2531 </TopContextProvider>,
2532 );
2533
2534 await waitForAll(['count:0']);
2535 assertConsoleErrorDev([
2536 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2537 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2538 ' in TopContextProvider (at **)',
2539 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2540 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2541 ' in MiddleContextProvider (at **)',
2542 'Child uses the legacy contextTypes API which will soon be removed. ' +
2543 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2544 ' in Child (at **)',
2545 ]);
2546 instance.updateCount();
2547 await waitForAll(['count:1']);
2548 });
2549
2550 // @gate !disableLegacyContext
2551 it('should not update descendants with new context values if shouldComponentUpdate returns false', async () => {
2552 let instance;
2553
2554 class TopContextProvider extends React.Component {
2555 static childContextTypes = {
2556 count: PropTypes.number,
2557 };
2558 constructor() {
2559 super();
2560 this.state = {count: 0};
2561 instance = this;
2562 }
2563 getChildContext = () => ({
2564 count: this.state.count,
2565 });
2566 render = () => this.props.children;
2567 updateCount = () =>
2568 this.setState(state => ({
2569 count: state.count + 1,
2570 }));
2571 }
2572
2573 class MiddleScu extends React.Component {
2574 shouldComponentUpdate() {
2575 return false;
2576 }
2577 render = () => this.props.children;
2578 }
2579
2580 class MiddleContextProvider extends React.Component {
2581 static childContextTypes = {
2582 name: PropTypes.string,
2583 };
2584 getChildContext = () => ({
2585 name: 'brian',
2586 });
2587 render = () => this.props.children;
2588 }
2589
2590 class Child extends React.Component {
2591 static contextTypes = {
2592 count: PropTypes.number,
2593 };
2594 render = () => {
2595 Scheduler.log(`count:${this.context.count}`);
2596 return null;
2597 };
2598 }
2599
2600 ReactNoop.render(
2601 <TopContextProvider>
2602 <MiddleScu>
2603 <MiddleContextProvider>
2604 <Child />
2605 </MiddleContextProvider>
2606 </MiddleScu>
2607 </TopContextProvider>,
2608 );
2609
2610 await waitForAll(['count:0']);
2611 assertConsoleErrorDev([
2612 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2613 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2614 ' in TopContextProvider (at **)',
2615 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2616 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2617 ' in MiddleContextProvider (at **)',
2618 'Child uses the legacy contextTypes API which will soon be removed. ' +
2619 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2620 ' in Child (at **)',
2621 ]);
2622 instance.updateCount();
2623 await waitForAll([]);
2624 });
2625
2626 // @gate !disableLegacyContext
2627 it('should update descendants with new context values if setState() is called in the middle of the tree', async () => {
2628 let middleInstance;
2629 let topInstance;
2630
2631 class TopContextProvider extends React.Component {
2632 static childContextTypes = {
2633 count: PropTypes.number,
2634 };
2635 constructor() {
2636 super();
2637 this.state = {count: 0};
2638 topInstance = this;
2639 }
2640 getChildContext = () => ({
2641 count: this.state.count,
2642 });
2643 render = () => this.props.children;
2644 updateCount = () =>
2645 this.setState(state => ({
2646 count: state.count + 1,
2647 }));
2648 }
2649
2650 class MiddleScu extends React.Component {
2651 shouldComponentUpdate() {
2652 return false;
2653 }
2654 render = () => this.props.children;
2655 }
2656
2657 class MiddleContextProvider extends React.Component {
2658 static childContextTypes = {
2659 name: PropTypes.string,
2660 };
2661 constructor() {
2662 super();
2663 this.state = {name: 'brian'};
2664 middleInstance = this;
2665 }
2666 getChildContext = () => ({
2667 name: this.state.name,
2668 });
2669 updateName = name => {
2670 this.setState({name});
2671 };
2672 render = () => this.props.children;
2673 }
2674
2675 class Child extends React.Component {
2676 static contextTypes = {
2677 count: PropTypes.number,
2678 name: PropTypes.string,
2679 };
2680 render = () => {
2681 Scheduler.log(`count:${this.context.count}, name:${this.context.name}`);
2682 return null;
2683 };
2684 }
2685
2686 ReactNoop.render(
2687 <TopContextProvider>
2688 <MiddleScu>
2689 <MiddleContextProvider>
2690 <Child />
2691 </MiddleContextProvider>
2692 </MiddleScu>
2693 </TopContextProvider>,
2694 );
2695
2696 await waitForAll(['count:0, name:brian']);
2697 assertConsoleErrorDev([
2698 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2699 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2700 ' in TopContextProvider (at **)',
2701 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
2702 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
2703 ' in MiddleContextProvider (at **)',
2704 'Child uses the legacy contextTypes API which will soon be removed. ' +
2705 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2706 ' in Child (at **)',
2707 ]);
2708 topInstance.updateCount();
2709 await waitForAll([]);
2710 middleInstance.updateName('not brian');
2711 await waitForAll(['count:1, name:not brian']);
2712 });
2713
2714 it('does not interrupt for update at same priority', async () => {
2715 function Parent(props) {
2716 Scheduler.log('Parent: ' + props.step);
2717 return <Child step={props.step} />;
2718 }
2719
2720 function Child(props) {
2721 Scheduler.log('Child: ' + props.step);
2722 return null;
2723 }
2724
2725 React.startTransition(() => {
2726 ReactNoop.render(<Parent step={1} />);
2727 });
2728 await waitFor(['Parent: 1']);
2729
2730 // Interrupt at same priority
2731 ReactNoop.render(<Parent step={2} />);
2732
2733 await waitForAll(['Child: 1', 'Parent: 2', 'Child: 2']);
2734 });
2735
2736 it('does not interrupt for update at lower priority', async () => {
2737 function Parent(props) {
2738 Scheduler.log('Parent: ' + props.step);
2739 return <Child step={props.step} />;
2740 }
2741
2742 function Child(props) {
2743 Scheduler.log('Child: ' + props.step);
2744 return null;
2745 }
2746
2747 React.startTransition(() => {
2748 ReactNoop.render(<Parent step={1} />);
2749 });
2750 await waitFor(['Parent: 1']);
2751
2752 // Interrupt at lower priority
2753 ReactNoop.expire(2000);
2754 ReactNoop.render(<Parent step={2} />);
2755
2756 await waitForAll(['Child: 1', 'Parent: 2', 'Child: 2']);
2757 });
2758
2759 it('does interrupt for update at higher priority', async () => {
2760 function Parent(props) {
2761 Scheduler.log('Parent: ' + props.step);
2762 return <Child step={props.step} />;
2763 }
2764
2765 function Child(props) {
2766 Scheduler.log('Child: ' + props.step);
2767 return null;
2768 }
2769
2770 React.startTransition(() => {
2771 ReactNoop.render(<Parent step={1} />);
2772 });
2773 await waitFor(['Parent: 1']);
2774
2775 // Interrupt at higher priority
2776 ReactNoop.flushSync(() => ReactNoop.render(<Parent step={2} />));
2777 assertLog(['Parent: 2', 'Child: 2']);
2778
2779 await waitForAll([]);
2780 });
2781
2782 // We sometimes use Maps with Fibers as keys.
2783 // @gate !disableLegacyContext || !__DEV__
2784 it('does not break with a bad Map polyfill', async () => {
2785 const realMapSet = Map.prototype.set;
2786
2787 async function triggerCodePathThatUsesFibersAsMapKeys() {
2788 function Thing() {
2789 throw new Error('No.');
2790 }
2791 // This class uses legacy context, which triggers warnings,
2792 // the procedures for which use a Map to store fibers.
2793 class Boundary extends React.Component {
2794 state = {didError: false};
2795 componentDidCatch() {
2796 this.setState({didError: true});
2797 }
2798 static contextTypes = {
2799 color: () => null,
2800 };
2801 render() {
2802 return this.state.didError ? null : <Thing />;
2803 }
2804 }
2805 ReactNoop.render(
2806 <React.StrictMode>
2807 <Boundary />
2808 </React.StrictMode>,
2809 );
2810 await waitForAll([]);
2811 assertConsoleErrorDev([
2812 'Boundary uses the legacy contextTypes API which will soon be removed. ' +
2813 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
2814 ' in Boundary (at **)',
2815 'Legacy context API has been detected within a strict-mode tree.\n' +
2816 '\n' +
2817 'The old API will be supported in all 16.x releases, but applications using it should migrate to the new version.\n' +
2818 '\n' +
2819 'Please update the following components: Boundary\n' +
2820 '\n' +
2821 'Learn more about this warning here: https://react.dev/link/legacy-context\n' +
2822 ' in Boundary (at **)',
2823 ]);
2824 }
2825
2826 // First, verify that this code path normally receives Fibers as keys,
2827 // and that they're not extensible.
2828 jest.resetModules();
2829 let receivedNonExtensibleObjects;
2830 // eslint-disable-next-line no-extend-native
2831 Map.prototype.set = function (key) {
2832 if (typeof key === 'object' && key !== null) {
2833 if (!Object.isExtensible(key)) {
2834 receivedNonExtensibleObjects = true;
2835 }
2836 }
2837 return realMapSet.apply(this, arguments);
2838 };
2839 React = require('react');
2840 ReactNoop = require('react-noop-renderer');
2841 Scheduler = require('scheduler');
2842 let InternalTestUtils = require('internal-test-utils');
2843 waitForAll = InternalTestUtils.waitForAll;
2844 waitFor = InternalTestUtils.waitFor;
2845 waitForThrow = InternalTestUtils.waitForThrow;
2846 assertLog = InternalTestUtils.assertLog;
2847
2848 try {
2849 receivedNonExtensibleObjects = false;
2850 await triggerCodePathThatUsesFibersAsMapKeys();
2851 } finally {
2852 // eslint-disable-next-line no-extend-native
2853 Map.prototype.set = realMapSet;
2854 }
2855 // If this fails, find another code path in Fiber
2856 // that passes Fibers as keys to Maps.
2857 // Note that we only expect them to be non-extensible
2858 // in development.
2859 expect(receivedNonExtensibleObjects).toBe(__DEV__);
2860
2861 // Next, verify that a Map polyfill that "writes" to keys
2862 // doesn't cause a failure.
2863 jest.resetModules();
2864 // eslint-disable-next-line no-extend-native
2865 Map.prototype.set = function (key, value) {
2866 if (typeof key === 'object' && key !== null) {
2867 // A polyfill could do something like this.
2868 // It would throw if an object is not extensible.
2869 key.__internalValueSlot = value;
2870 }
2871 return realMapSet.apply(this, arguments);
2872 };
2873 React = require('react');
2874 ReactNoop = require('react-noop-renderer');
2875 Scheduler = require('scheduler');
2876 InternalTestUtils = require('internal-test-utils');
2877 waitForAll = InternalTestUtils.waitForAll;
2878 waitFor = InternalTestUtils.waitFor;
2879 waitForThrow = InternalTestUtils.waitForThrow;
2880 assertLog = InternalTestUtils.assertLog;
2881
2882 try {
2883 await triggerCodePathThatUsesFibersAsMapKeys();
2884 } finally {
2885 // eslint-disable-next-line no-extend-native
2886 Map.prototype.set = realMapSet;
2887 }
2888 // If we got this far, our feature detection worked.
2889 // We knew that Map#set() throws for non-extensible objects,
2890 // so we didn't set them as non-extensible for that reason.
2891 });
2892 });