main
js 1,672 lines 49 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 */
9
10 'use strict';
11
12 let React = require('react');
13 let useContext;
14 let ReactNoop;
15 let Scheduler;
16 let gen;
17 let waitForAll;
18 let waitFor;
19 let waitForThrow;
20 let assertConsoleErrorDev;
21
22 describe('ReactNewContext', () => {
23 beforeEach(() => {
24 jest.resetModules();
25
26 React = require('react');
27 useContext = React.useContext;
28 ReactNoop = require('react-noop-renderer');
29 Scheduler = require('scheduler');
30 gen = require('random-seed');
31
32 ({
33 waitForAll,
34 waitFor,
35 waitForThrow,
36 assertConsoleErrorDev,
37 } = require('internal-test-utils'));
38 });
39
40 afterEach(() => {
41 jest.restoreAllMocks();
42 });
43
44 function Text(props) {
45 Scheduler.log(props.text);
46 return <span prop={props.text} />;
47 }
48
49 function span(prop) {
50 return {type: 'span', children: [], prop, hidden: false};
51 }
52
53 function readContext(Context) {
54 const dispatcher =
55 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H;
56 return dispatcher.readContext(Context);
57 }
58
59 // Note: This is based on a similar component we use in www. We can delete
60 // once the extra div wrapper is no longer necessary.
61 function LegacyHiddenDiv({children, mode}) {
62 return (
63 <div hidden={mode === 'hidden'}>
64 <React.unstable_LegacyHidden
65 mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
66 {children}
67 </React.unstable_LegacyHidden>
68 </div>
69 );
70 }
71
72 // We have several ways of reading from context. sharedContextTests runs
73 // a suite of tests for a given context consumer implementation.
74 sharedContextTests('Context.Consumer', Context => Context.Consumer);
75 sharedContextTests(
76 'useContext inside function component',
77 Context =>
78 function Consumer(props) {
79 const contextValue = useContext(Context);
80 const render = props.children;
81 return render(contextValue);
82 },
83 );
84 sharedContextTests('useContext inside forwardRef component', Context =>
85 React.forwardRef(function Consumer(props, ref) {
86 const contextValue = useContext(Context);
87 const render = props.children;
88 return render(contextValue);
89 }),
90 );
91 sharedContextTests('useContext inside memoized function component', Context =>
92 React.memo(function Consumer(props) {
93 const contextValue = useContext(Context);
94 const render = props.children;
95 return render(contextValue);
96 }),
97 );
98 sharedContextTests(
99 'readContext(Context) inside class component',
100 Context =>
101 class Consumer extends React.Component {
102 render() {
103 const contextValue = readContext(Context);
104 const render = this.props.children;
105 return render(contextValue);
106 }
107 },
108 );
109 sharedContextTests(
110 'readContext(Context) inside pure class component',
111 Context =>
112 class Consumer extends React.PureComponent {
113 render() {
114 const contextValue = readContext(Context);
115 const render = this.props.children;
116 return render(contextValue);
117 }
118 },
119 );
120
121 function sharedContextTests(label, getConsumer) {
122 describe(`reading context with ${label}`, () => {
123 it('simple mount and update', async () => {
124 const Context = React.createContext(1);
125 const Consumer = getConsumer(Context);
126
127 const Indirection = React.Fragment;
128
129 function App(props) {
130 return (
131 <Context.Provider value={props.value}>
132 <Indirection>
133 <Indirection>
134 <Consumer>
135 {value => <span prop={'Result: ' + value} />}
136 </Consumer>
137 </Indirection>
138 </Indirection>
139 </Context.Provider>
140 );
141 }
142
143 ReactNoop.render(<App value={2} />);
144 await waitForAll([]);
145 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 2" />);
146
147 // Update
148 ReactNoop.render(<App value={3} />);
149 await waitForAll([]);
150 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 3" />);
151 });
152
153 it('propagates through shouldComponentUpdate false', async () => {
154 const Context = React.createContext(1);
155 const ContextConsumer = getConsumer(Context);
156
157 function Provider(props) {
158 Scheduler.log('Provider');
159 return (
160 <Context.Provider value={props.value}>
161 {props.children}
162 </Context.Provider>
163 );
164 }
165
166 function Consumer(props) {
167 Scheduler.log('Consumer');
168 return (
169 <ContextConsumer>
170 {value => {
171 Scheduler.log('Consumer render prop');
172 return <span prop={'Result: ' + value} />;
173 }}
174 </ContextConsumer>
175 );
176 }
177
178 class Indirection extends React.Component {
179 shouldComponentUpdate() {
180 return false;
181 }
182 render() {
183 Scheduler.log('Indirection');
184 return this.props.children;
185 }
186 }
187
188 function App(props) {
189 Scheduler.log('App');
190 return (
191 <Provider value={props.value}>
192 <Indirection>
193 <Indirection>
194 <Consumer />
195 </Indirection>
196 </Indirection>
197 </Provider>
198 );
199 }
200
201 ReactNoop.render(<App value={2} />);
202 await waitForAll([
203 'App',
204 'Provider',
205 'Indirection',
206 'Indirection',
207 'Consumer',
208 'Consumer render prop',
209 ]);
210 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 2" />);
211
212 // Update
213 ReactNoop.render(<App value={3} />);
214 await waitForAll(['App', 'Provider', 'Consumer render prop']);
215 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 3" />);
216 });
217
218 it('consumers bail out if context value is the same', async () => {
219 const Context = React.createContext(1);
220 const ContextConsumer = getConsumer(Context);
221
222 function Provider(props) {
223 Scheduler.log('Provider');
224 return (
225 <Context.Provider value={props.value}>
226 {props.children}
227 </Context.Provider>
228 );
229 }
230
231 function Consumer(props) {
232 Scheduler.log('Consumer');
233 return (
234 <ContextConsumer>
235 {value => {
236 Scheduler.log('Consumer render prop');
237 return <span prop={'Result: ' + value} />;
238 }}
239 </ContextConsumer>
240 );
241 }
242
243 class Indirection extends React.Component {
244 shouldComponentUpdate() {
245 return false;
246 }
247 render() {
248 Scheduler.log('Indirection');
249 return this.props.children;
250 }
251 }
252
253 function App(props) {
254 Scheduler.log('App');
255 return (
256 <Provider value={props.value}>
257 <Indirection>
258 <Indirection>
259 <Consumer />
260 </Indirection>
261 </Indirection>
262 </Provider>
263 );
264 }
265
266 ReactNoop.render(<App value={2} />);
267 await waitForAll([
268 'App',
269 'Provider',
270 'Indirection',
271 'Indirection',
272 'Consumer',
273 'Consumer render prop',
274 ]);
275 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 2" />);
276
277 // Update with the same context value
278 ReactNoop.render(<App value={2} />);
279 await waitForAll([
280 'App',
281 'Provider',
282 // Don't call render prop again
283 ]);
284 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 2" />);
285 });
286
287 it('nested providers', async () => {
288 const Context = React.createContext(1);
289 const Consumer = getConsumer(Context);
290
291 function Provider(props) {
292 return (
293 <Consumer>
294 {contextValue => (
295 // Multiply previous context value by 2, unless prop overrides
296 <Context.Provider value={props.value || contextValue * 2}>
297 {props.children}
298 </Context.Provider>
299 )}
300 </Consumer>
301 );
302 }
303
304 class Indirection extends React.Component {
305 shouldComponentUpdate() {
306 return false;
307 }
308 render() {
309 return this.props.children;
310 }
311 }
312
313 function App(props) {
314 return (
315 <Provider value={props.value}>
316 <Indirection>
317 <Provider>
318 <Indirection>
319 <Provider>
320 <Indirection>
321 <Consumer>
322 {value => <span prop={'Result: ' + value} />}
323 </Consumer>
324 </Indirection>
325 </Provider>
326 </Indirection>
327 </Provider>
328 </Indirection>
329 </Provider>
330 );
331 }
332
333 ReactNoop.render(<App value={2} />);
334 await waitForAll([]);
335 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 8" />);
336
337 // Update
338 ReactNoop.render(<App value={3} />);
339 await waitForAll([]);
340 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: 12" />);
341 });
342
343 it('should provide the correct (default) values to consumers outside of a provider', async () => {
344 const FooContext = React.createContext({value: 'foo-initial'});
345 const BarContext = React.createContext({value: 'bar-initial'});
346 const FooConsumer = getConsumer(FooContext);
347 const BarConsumer = getConsumer(BarContext);
348
349 const Verify = ({actual, expected}) => {
350 expect(expected).toBe(actual);
351 return null;
352 };
353
354 ReactNoop.render(
355 <>
356 <BarContext.Provider value={{value: 'bar-updated'}}>
357 <BarConsumer>
358 {({value}) => <Verify actual={value} expected="bar-updated" />}
359 </BarConsumer>
360
361 <FooContext.Provider value={{value: 'foo-updated'}}>
362 <FooConsumer>
363 {({value}) => (
364 <Verify actual={value} expected="foo-updated" />
365 )}
366 </FooConsumer>
367 </FooContext.Provider>
368 </BarContext.Provider>
369
370 <FooConsumer>
371 {({value}) => <Verify actual={value} expected="foo-initial" />}
372 </FooConsumer>
373 <BarConsumer>
374 {({value}) => <Verify actual={value} expected="bar-initial" />}
375 </BarConsumer>
376 </>,
377 );
378 await waitForAll([]);
379 });
380
381 it('multiple consumers in different branches', async () => {
382 const Context = React.createContext(1);
383 const Consumer = getConsumer(Context);
384
385 function Provider(props) {
386 return (
387 <Context.Consumer>
388 {contextValue => (
389 // Multiply previous context value by 2, unless prop overrides
390 <Context.Provider value={props.value || contextValue * 2}>
391 {props.children}
392 </Context.Provider>
393 )}
394 </Context.Consumer>
395 );
396 }
397
398 class Indirection extends React.Component {
399 shouldComponentUpdate() {
400 return false;
401 }
402 render() {
403 return this.props.children;
404 }
405 }
406
407 function App(props) {
408 return (
409 <Provider value={props.value}>
410 <Indirection>
411 <Indirection>
412 <Provider>
413 <Consumer>
414 {value => <span prop={'Result: ' + value} />}
415 </Consumer>
416 </Provider>
417 </Indirection>
418 <Indirection>
419 <Consumer>
420 {value => <span prop={'Result: ' + value} />}
421 </Consumer>
422 </Indirection>
423 </Indirection>
424 </Provider>
425 );
426 }
427
428 ReactNoop.render(<App value={2} />);
429 await waitForAll([]);
430 expect(ReactNoop).toMatchRenderedOutput(
431 <>
432 <span prop="Result: 4" />
433 <span prop="Result: 2" />
434 </>,
435 );
436
437 // Update
438 ReactNoop.render(<App value={3} />);
439 await waitForAll([]);
440 expect(ReactNoop).toMatchRenderedOutput(
441 <>
442 <span prop="Result: 6" />
443 <span prop="Result: 3" />
444 </>,
445 );
446
447 // Another update
448 ReactNoop.render(<App value={4} />);
449 await waitForAll([]);
450 expect(ReactNoop).toMatchRenderedOutput(
451 <>
452 <span prop="Result: 8" />
453 <span prop="Result: 4" />
454 </>,
455 );
456 });
457
458 it('compares context values with Object.is semantics', async () => {
459 const Context = React.createContext(1);
460 const ContextConsumer = getConsumer(Context);
461
462 function Provider(props) {
463 Scheduler.log('Provider');
464 return (
465 <Context.Provider value={props.value}>
466 {props.children}
467 </Context.Provider>
468 );
469 }
470
471 function Consumer(props) {
472 Scheduler.log('Consumer');
473 return (
474 <ContextConsumer>
475 {value => {
476 Scheduler.log('Consumer render prop');
477 return <span prop={'Result: ' + value} />;
478 }}
479 </ContextConsumer>
480 );
481 }
482
483 class Indirection extends React.Component {
484 shouldComponentUpdate() {
485 return false;
486 }
487 render() {
488 Scheduler.log('Indirection');
489 return this.props.children;
490 }
491 }
492
493 function App(props) {
494 Scheduler.log('App');
495 return (
496 <Provider value={props.value}>
497 <Indirection>
498 <Indirection>
499 <Consumer />
500 </Indirection>
501 </Indirection>
502 </Provider>
503 );
504 }
505
506 ReactNoop.render(<App value={NaN} />);
507 await waitForAll([
508 'App',
509 'Provider',
510 'Indirection',
511 'Indirection',
512 'Consumer',
513 'Consumer render prop',
514 ]);
515 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: NaN" />);
516
517 // Update
518 ReactNoop.render(<App value={NaN} />);
519 await waitForAll([
520 'App',
521 'Provider',
522 // Consumer should not re-render again
523 // 'Consumer render prop',
524 ]);
525 expect(ReactNoop).toMatchRenderedOutput(<span prop="Result: NaN" />);
526 });
527
528 it('context unwinds when interrupted', async () => {
529 const Context = React.createContext('Default');
530 const ContextConsumer = getConsumer(Context);
531
532 function Consumer(props) {
533 return (
534 <ContextConsumer>
535 {value => <span prop={'Result: ' + value} />}
536 </ContextConsumer>
537 );
538 }
539
540 function BadRender() {
541 throw new Error('Bad render');
542 }
543
544 class ErrorBoundary extends React.Component {
545 state = {error: null};
546 componentDidCatch(error) {
547 this.setState({error});
548 }
549 render() {
550 if (this.state.error) {
551 return null;
552 }
553 return this.props.children;
554 }
555 }
556
557 function App(props) {
558 return (
559 <>
560 <Context.Provider value="Does not unwind">
561 <ErrorBoundary>
562 <Context.Provider value="Unwinds after BadRender throws">
563 <BadRender />
564 </Context.Provider>
565 </ErrorBoundary>
566 <Consumer />
567 </Context.Provider>
568 </>
569 );
570 }
571
572 ReactNoop.render(<App value="A" />);
573 await waitForAll([]);
574 expect(ReactNoop).toMatchRenderedOutput(
575 // The second provider should use the default value.
576 <span prop="Result: Does not unwind" />,
577 );
578 });
579
580 it("does not re-render if there's an update in a child", async () => {
581 const Context = React.createContext(0);
582 const Consumer = getConsumer(Context);
583
584 let child;
585 class Child extends React.Component {
586 state = {step: 0};
587 render() {
588 Scheduler.log('Child');
589 return (
590 <span
591 prop={`Context: ${this.props.context}, Step: ${this.state.step}`}
592 />
593 );
594 }
595 }
596
597 function App(props) {
598 return (
599 <Context.Provider value={props.value}>
600 <Consumer>
601 {value => {
602 Scheduler.log('Consumer render prop');
603 return <Child ref={inst => (child = inst)} context={value} />;
604 }}
605 </Consumer>
606 </Context.Provider>
607 );
608 }
609
610 // Initial mount
611 ReactNoop.render(<App value={1} />);
612 await waitForAll(['Consumer render prop', 'Child']);
613 expect(ReactNoop).toMatchRenderedOutput(
614 <span prop="Context: 1, Step: 0" />,
615 );
616
617 child.setState({step: 1});
618 await waitForAll(['Child']);
619 expect(ReactNoop).toMatchRenderedOutput(
620 <span prop="Context: 1, Step: 1" />,
621 );
622 });
623
624 it('consumer bails out if value is unchanged and something above bailed out', async () => {
625 const Context = React.createContext(0);
626 const Consumer = getConsumer(Context);
627
628 function renderChildValue(value) {
629 Scheduler.log('Consumer');
630 return <span prop={value} />;
631 }
632
633 function ChildWithInlineRenderCallback() {
634 Scheduler.log('ChildWithInlineRenderCallback');
635 // Note: we are intentionally passing an inline arrow. Don't refactor.
636 return <Consumer>{value => renderChildValue(value)}</Consumer>;
637 }
638
639 function ChildWithCachedRenderCallback() {
640 Scheduler.log('ChildWithCachedRenderCallback');
641 return <Consumer>{renderChildValue}</Consumer>;
642 }
643
644 class PureIndirection extends React.PureComponent {
645 render() {
646 Scheduler.log('PureIndirection');
647 return (
648 <>
649 <ChildWithInlineRenderCallback />
650 <ChildWithCachedRenderCallback />
651 </>
652 );
653 }
654 }
655
656 class App extends React.Component {
657 render() {
658 Scheduler.log('App');
659 return (
660 <Context.Provider value={this.props.value}>
661 <PureIndirection />
662 </Context.Provider>
663 );
664 }
665 }
666
667 // Initial mount
668 ReactNoop.render(<App value={1} />);
669 await waitForAll([
670 'App',
671 'PureIndirection',
672 'ChildWithInlineRenderCallback',
673 'Consumer',
674 'ChildWithCachedRenderCallback',
675 'Consumer',
676 ]);
677 expect(ReactNoop).toMatchRenderedOutput(
678 <>
679 <span prop={1} />
680 <span prop={1} />
681 </>,
682 );
683
684 // Update (bailout)
685 ReactNoop.render(<App value={1} />);
686 await waitForAll(['App']);
687 expect(ReactNoop).toMatchRenderedOutput(
688 <>
689 <span prop={1} />
690 <span prop={1} />
691 </>,
692 );
693
694 // Update (no bailout)
695 ReactNoop.render(<App value={2} />);
696 await waitForAll(['App', 'Consumer', 'Consumer']);
697 expect(ReactNoop).toMatchRenderedOutput(
698 <>
699 <span prop={2} />
700 <span prop={2} />
701 </>,
702 );
703 });
704
705 // @gate enableLegacyHidden
706 it("context consumer doesn't bail out inside hidden subtree", async () => {
707 const Context = React.createContext('dark');
708 const Consumer = getConsumer(Context);
709
710 function App({theme}) {
711 return (
712 <Context.Provider value={theme}>
713 <LegacyHiddenDiv mode="hidden">
714 <Consumer>{value => <Text text={value} />}</Consumer>
715 </LegacyHiddenDiv>
716 </Context.Provider>
717 );
718 }
719
720 ReactNoop.render(<App theme="dark" />);
721 await waitForAll(['dark']);
722 expect(ReactNoop.getChildrenAsJSX()).toEqual(
723 <div hidden={true}>
724 <span prop="dark" />
725 </div>,
726 );
727
728 ReactNoop.render(<App theme="light" />);
729 await waitForAll(['light']);
730 expect(ReactNoop.getChildrenAsJSX()).toEqual(
731 <div hidden={true}>
732 <span prop="light" />
733 </div>,
734 );
735 });
736
737 // This is a regression case for https://github.com/facebook/react/issues/12389.
738 it('does not run into an infinite loop', async () => {
739 const Context = React.createContext(null);
740 const Consumer = getConsumer(Context);
741
742 class App extends React.Component {
743 renderItem(id) {
744 return (
745 <span key={id}>
746 <Consumer>{() => <span>inner</span>}</Consumer>
747 <span>outer</span>
748 </span>
749 );
750 }
751 renderList() {
752 const list = [1, 2].map(id => this.renderItem(id));
753 if (this.props.reverse) {
754 list.reverse();
755 }
756 return list;
757 }
758 render() {
759 return (
760 <Context.Provider value={{}}>
761 {this.renderList()}
762 </Context.Provider>
763 );
764 }
765 }
766
767 ReactNoop.render(<App reverse={false} />);
768 await waitForAll([]);
769 ReactNoop.render(<App reverse={true} />);
770 await waitForAll([]);
771 ReactNoop.render(<App reverse={false} />);
772 await waitForAll([]);
773 });
774
775 // This is a regression case for https://github.com/facebook/react/issues/12686
776 it('does not skip some siblings', async () => {
777 const Context = React.createContext(0);
778 const ContextConsumer = getConsumer(Context);
779
780 class App extends React.Component {
781 state = {
782 step: 0,
783 };
784
785 render() {
786 Scheduler.log('App');
787 return (
788 <Context.Provider value={this.state.step}>
789 <StaticContent />
790 {this.state.step > 0 && <Indirection />}
791 </Context.Provider>
792 );
793 }
794 }
795
796 class StaticContent extends React.PureComponent {
797 render() {
798 return (
799 <>
800 <>
801 <span prop="static 1" />
802 <span prop="static 2" />
803 </>
804 </>
805 );
806 }
807 }
808
809 class Indirection extends React.PureComponent {
810 render() {
811 return (
812 <ContextConsumer>
813 {value => {
814 Scheduler.log('Consumer');
815 return <span prop={value} />;
816 }}
817 </ContextConsumer>
818 );
819 }
820 }
821
822 // Initial mount
823 let inst;
824 ReactNoop.render(<App ref={ref => (inst = ref)} />);
825 await waitForAll(['App']);
826 expect(ReactNoop).toMatchRenderedOutput(
827 <>
828 <span prop="static 1" />
829 <span prop="static 2" />
830 </>,
831 );
832 // Update the first time
833 inst.setState({step: 1});
834 await waitForAll(['App', 'Consumer']);
835 expect(ReactNoop).toMatchRenderedOutput(
836 <>
837 <span prop="static 1" />
838 <span prop="static 2" />
839 <span prop={1} />
840 </>,
841 );
842 // Update the second time
843 inst.setState({step: 2});
844 await waitForAll(['App', 'Consumer']);
845 expect(ReactNoop).toMatchRenderedOutput(
846 <>
847 <span prop="static 1" />
848 <span prop="static 2" />
849 <span prop={2} />
850 </>,
851 );
852 });
853 });
854 }
855
856 describe('Context.Provider', () => {
857 it('warns if no value prop provided', async () => {
858 const Context = React.createContext();
859
860 ReactNoop.render(
861 <Context.Provider anyPropNameOtherThanValue="value could be anything" />,
862 );
863
864 await waitForAll([]);
865 assertConsoleErrorDev([
866 'The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?',
867 ]);
868 });
869
870 it('warns if multiple renderers concurrently render the same context', async () => {
871 spyOnDev(console, 'error').mockImplementation(() => {});
872 const Context = React.createContext(0);
873
874 function Foo(props) {
875 Scheduler.log('Foo');
876 return null;
877 }
878
879 function App(props) {
880 return (
881 <Context.Provider value={props.value}>
882 <Foo />
883 <Foo />
884 </Context.Provider>
885 );
886 }
887
888 React.startTransition(() => {
889 ReactNoop.render(<App value={1} />);
890 });
891 // Render past the Provider, but don't commit yet
892 await waitFor(['Foo']);
893
894 // Get a new copy of ReactNoop
895 jest.resetModules();
896 React = require('react');
897 ReactNoop = require('react-noop-renderer');
898 Scheduler = require('scheduler');
899 const InternalTestUtils = require('internal-test-utils');
900 waitForAll = InternalTestUtils.waitForAll;
901 waitFor = InternalTestUtils.waitFor;
902
903 // Render the provider again using a different renderer
904 ReactNoop.render(<App value={1} />);
905 await waitForAll(['Foo', 'Foo']);
906
907 if (__DEV__) {
908 expect(console.error.mock.calls[0][0]).toContain(
909 'Detected multiple renderers concurrently rendering the same ' +
910 'context provider. This is currently unsupported',
911 );
912 }
913 });
914
915 it('does not warn if multiple renderers use the same context sequentially', async () => {
916 spyOnDev(console, 'error');
917 const Context = React.createContext(0);
918
919 function Foo(props) {
920 Scheduler.log('Foo');
921 return null;
922 }
923
924 function App(props) {
925 return (
926 <Context.Provider value={props.value}>
927 <Foo />
928 <Foo />
929 </Context.Provider>
930 );
931 }
932
933 React.startTransition(() => {
934 ReactNoop.render(<App value={1} />);
935 });
936 await waitForAll(['Foo', 'Foo']);
937
938 // Get a new copy of ReactNoop
939 jest.resetModules();
940 React = require('react');
941 ReactNoop = require('react-noop-renderer');
942 Scheduler = require('scheduler');
943 const InternalTestUtils = require('internal-test-utils');
944 waitForAll = InternalTestUtils.waitForAll;
945 waitFor = InternalTestUtils.waitFor;
946
947 // Render the provider again using a different renderer
948 ReactNoop.render(<App value={1} />);
949 await waitForAll(['Foo', 'Foo']);
950
951 if (__DEV__) {
952 expect(console.error).not.toHaveBeenCalled();
953 }
954 });
955
956 it('provider bails out if children and value are unchanged (like sCU)', async () => {
957 const Context = React.createContext(0);
958
959 function Child() {
960 Scheduler.log('Child');
961 return <span prop="Child" />;
962 }
963
964 const children = <Child />;
965
966 function App(props) {
967 Scheduler.log('App');
968 return (
969 <Context.Provider value={props.value}>{children}</Context.Provider>
970 );
971 }
972
973 // Initial mount
974 ReactNoop.render(<App value={1} />);
975 await waitForAll(['App', 'Child']);
976 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
977
978 // Update
979 ReactNoop.render(<App value={1} />);
980 await waitForAll([
981 'App',
982 // Child does not re-render
983 ]);
984 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
985 });
986
987 // @gate !disableLegacyContext
988 it('provider does not bail out if legacy context changed above', async () => {
989 const Context = React.createContext(0);
990
991 function Child() {
992 Scheduler.log('Child');
993 return <span prop="Child" />;
994 }
995
996 const children = <Child />;
997
998 class LegacyProvider extends React.Component {
999 static childContextTypes = {
1000 legacyValue: () => {},
1001 };
1002 state = {legacyValue: 1};
1003 getChildContext() {
1004 return {legacyValue: this.state.legacyValue};
1005 }
1006 render() {
1007 Scheduler.log('LegacyProvider');
1008 return this.props.children;
1009 }
1010 }
1011
1012 class App extends React.Component {
1013 state = {value: 1};
1014 render() {
1015 Scheduler.log('App');
1016 return (
1017 <Context.Provider value={this.state.value}>
1018 {this.props.children}
1019 </Context.Provider>
1020 );
1021 }
1022 }
1023
1024 const legacyProviderRef = React.createRef();
1025 const appRef = React.createRef();
1026
1027 // Initial mount
1028 ReactNoop.render(
1029 <LegacyProvider ref={legacyProviderRef}>
1030 <App ref={appRef} value={1}>
1031 {children}
1032 </App>
1033 </LegacyProvider>,
1034 );
1035 await waitForAll(['LegacyProvider', 'App', 'Child']);
1036 assertConsoleErrorDev([
1037 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. ' +
1038 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
1039 ' in LegacyProvider (at **)',
1040 ]);
1041 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
1042
1043 // Update App with same value (should bail out)
1044 appRef.current.setState({value: 1});
1045 await waitForAll(['App']);
1046 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
1047
1048 // Update LegacyProvider (should not bail out)
1049 legacyProviderRef.current.setState({value: 1});
1050 await waitForAll(['LegacyProvider', 'App', 'Child']);
1051 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
1052
1053 // Update App with same value (should bail out)
1054 appRef.current.setState({value: 1});
1055 await waitForAll(['App']);
1056 expect(ReactNoop).toMatchRenderedOutput(<span prop="Child" />);
1057 });
1058 });
1059
1060 describe('Context.Consumer', () => {
1061 it('warns if child is not a function', async () => {
1062 spyOnDev(console, 'error').mockImplementation(() => {});
1063 const Context = React.createContext(0);
1064 ReactNoop.render(<Context.Consumer />);
1065 await waitForThrow('is not a function');
1066 if (__DEV__) {
1067 expect(console.error.mock.calls[0][0]).toContain(
1068 'A context consumer was rendered with multiple children, or a child ' +
1069 "that isn't a function",
1070 );
1071 }
1072 });
1073
1074 it('can read other contexts inside consumer render prop', async () => {
1075 const FooContext = React.createContext(0);
1076 const BarContext = React.createContext(0);
1077
1078 function FooAndBar() {
1079 return (
1080 <FooContext.Consumer>
1081 {foo => {
1082 const bar = readContext(BarContext);
1083 return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1084 }}
1085 </FooContext.Consumer>
1086 );
1087 }
1088
1089 class Indirection extends React.Component {
1090 shouldComponentUpdate() {
1091 return false;
1092 }
1093 render() {
1094 return this.props.children;
1095 }
1096 }
1097
1098 function App(props) {
1099 return (
1100 <FooContext.Provider value={props.foo}>
1101 <BarContext.Provider value={props.bar}>
1102 <Indirection>
1103 <FooAndBar />
1104 </Indirection>
1105 </BarContext.Provider>
1106 </FooContext.Provider>
1107 );
1108 }
1109
1110 ReactNoop.render(<App foo={1} bar={1} />);
1111 await waitForAll(['Foo: 1, Bar: 1']);
1112 expect(ReactNoop).toMatchRenderedOutput(<span prop="Foo: 1, Bar: 1" />);
1113
1114 // Update foo
1115 ReactNoop.render(<App foo={2} bar={1} />);
1116 await waitForAll(['Foo: 2, Bar: 1']);
1117 expect(ReactNoop).toMatchRenderedOutput(<span prop="Foo: 2, Bar: 1" />);
1118
1119 // Update bar
1120 ReactNoop.render(<App foo={2} bar={2} />);
1121 await waitForAll(['Foo: 2, Bar: 2']);
1122 expect(ReactNoop).toMatchRenderedOutput(<span prop="Foo: 2, Bar: 2" />);
1123 });
1124
1125 // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1126 // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1127 // If we bailed out on referential equality, it would be confusing that you
1128 // can call this.setState(), but an autobound render callback "blocked" the update.
1129 // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1130 it('consumer does not bail out if there were no bailouts above it', async () => {
1131 const Context = React.createContext(0);
1132 const Consumer = Context.Consumer;
1133
1134 class App extends React.Component {
1135 state = {
1136 text: 'hello',
1137 };
1138
1139 renderConsumer = context => {
1140 Scheduler.log('App#renderConsumer');
1141 return <span prop={this.state.text} />;
1142 };
1143
1144 render() {
1145 Scheduler.log('App');
1146 return (
1147 <Context.Provider value={this.props.value}>
1148 <Consumer>{this.renderConsumer}</Consumer>
1149 </Context.Provider>
1150 );
1151 }
1152 }
1153
1154 // Initial mount
1155 let inst;
1156 ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1157 await waitForAll(['App', 'App#renderConsumer']);
1158 expect(ReactNoop).toMatchRenderedOutput(<span prop="hello" />);
1159
1160 // Update
1161 inst.setState({text: 'goodbye'});
1162 await waitForAll(['App', 'App#renderConsumer']);
1163 expect(ReactNoop).toMatchRenderedOutput(<span prop="goodbye" />);
1164 });
1165 });
1166
1167 describe('readContext', () => {
1168 // Unstable changedBits API was removed. Port this test to context selectors
1169 // once that exists.
1170 // @gate FIXME
1171 it('can read the same context multiple times in the same function', async () => {
1172 const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
1173 let result = 0;
1174 if (a.foo !== b.foo) {
1175 result |= 0b001;
1176 }
1177 if (a.bar !== b.bar) {
1178 result |= 0b010;
1179 }
1180 if (a.baz !== b.baz) {
1181 result |= 0b100;
1182 }
1183 return result;
1184 });
1185
1186 function Provider(props) {
1187 return (
1188 <Context.Provider
1189 value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
1190 {props.children}
1191 </Context.Provider>
1192 );
1193 }
1194
1195 function FooAndBar() {
1196 const {foo} = readContext(Context, 0b001);
1197 const {bar} = readContext(Context, 0b010);
1198 return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1199 }
1200
1201 function Baz() {
1202 const {baz} = readContext(Context, 0b100);
1203 return <Text text={'Baz: ' + baz} />;
1204 }
1205
1206 class Indirection extends React.Component {
1207 shouldComponentUpdate() {
1208 return false;
1209 }
1210 render() {
1211 return this.props.children;
1212 }
1213 }
1214
1215 function App(props) {
1216 return (
1217 <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
1218 <Indirection>
1219 <Indirection>
1220 <FooAndBar />
1221 </Indirection>
1222 <Indirection>
1223 <Baz />
1224 </Indirection>
1225 </Indirection>
1226 </Provider>
1227 );
1228 }
1229
1230 ReactNoop.render(<App foo={1} bar={1} baz={1} />);
1231 await waitForAll(['Foo: 1, Bar: 1', 'Baz: 1']);
1232 expect(ReactNoop).toMatchRenderedOutput([
1233 <span prop="Foo: 1, Bar: 1" />,
1234 <span prop="Baz: 1" />,
1235 ]);
1236
1237 // Update only foo
1238 ReactNoop.render(<App foo={2} bar={1} baz={1} />);
1239 await waitForAll(['Foo: 2, Bar: 1']);
1240 expect(ReactNoop).toMatchRenderedOutput([
1241 <span prop="Foo: 2, Bar: 1" />,
1242 <span prop="Baz: 1" />,
1243 ]);
1244
1245 // Update only bar
1246 ReactNoop.render(<App foo={2} bar={2} baz={1} />);
1247 await waitForAll(['Foo: 2, Bar: 2']);
1248 expect(ReactNoop).toMatchRenderedOutput([
1249 <span prop="Foo: 2, Bar: 2" />,
1250 <span prop="Baz: 1" />,
1251 ]);
1252
1253 // Update only baz
1254 ReactNoop.render(<App foo={2} bar={2} baz={2} />);
1255 await waitForAll(['Baz: 2']);
1256 expect(ReactNoop).toMatchRenderedOutput([
1257 <span prop="Foo: 2, Bar: 2" />,
1258 <span prop="Baz: 2" />,
1259 ]);
1260 });
1261
1262 // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1263 // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1264 // If we bailed out on referential equality, it would be confusing that you
1265 // can call this.setState(), but an autobound render callback "blocked" the update.
1266 // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1267 it('does not bail out if there were no bailouts above it', async () => {
1268 const Context = React.createContext(0);
1269
1270 class Consumer extends React.Component {
1271 render() {
1272 const contextValue = readContext(Context);
1273 return this.props.children(contextValue);
1274 }
1275 }
1276
1277 class App extends React.Component {
1278 state = {
1279 text: 'hello',
1280 };
1281
1282 renderConsumer = context => {
1283 Scheduler.log('App#renderConsumer');
1284 return <span prop={this.state.text} />;
1285 };
1286
1287 render() {
1288 Scheduler.log('App');
1289 return (
1290 <Context.Provider value={this.props.value}>
1291 <Consumer>{this.renderConsumer}</Consumer>
1292 </Context.Provider>
1293 );
1294 }
1295 }
1296
1297 // Initial mount
1298 let inst;
1299 ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1300 await waitForAll(['App', 'App#renderConsumer']);
1301 expect(ReactNoop).toMatchRenderedOutput(<span prop="hello" />);
1302
1303 // Update
1304 inst.setState({text: 'goodbye'});
1305 await waitForAll(['App', 'App#renderConsumer']);
1306 expect(ReactNoop).toMatchRenderedOutput(<span prop="goodbye" />);
1307 });
1308
1309 it('warns when reading context inside render phase class setState updater', async () => {
1310 const ThemeContext = React.createContext('light');
1311
1312 class Cls extends React.Component {
1313 state = {};
1314 render() {
1315 this.setState(() => {
1316 readContext(ThemeContext);
1317 });
1318 return null;
1319 }
1320 }
1321
1322 ReactNoop.render(<Cls />);
1323 await waitForAll([]);
1324 assertConsoleErrorDev([
1325 'Cannot update during an existing state transition (such as within `render`). ' +
1326 'Render methods should be a pure function of props and state.\n' +
1327 ' in Cls (at **)',
1328 'Context can only be read while React is rendering. ' +
1329 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1330 'In function components, you can read it directly in the function body, ' +
1331 'but not inside Hooks like useReducer() or useMemo().\n' +
1332 ' in Cls (at **)',
1333 ]);
1334 });
1335 });
1336
1337 describe('useContext', () => {
1338 it('throws when used in a class component', async () => {
1339 const Context = React.createContext(0);
1340 class Foo extends React.Component {
1341 render() {
1342 return useContext(Context);
1343 }
1344 }
1345 ReactNoop.render(<Foo />);
1346 await waitForThrow(
1347 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' +
1348 ' for one of the following reasons:\n' +
1349 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
1350 '2. You might be breaking the Rules of Hooks\n' +
1351 '3. You might have more than one copy of React in the same app\n' +
1352 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
1353 );
1354 });
1355
1356 it('warns when passed a consumer', async () => {
1357 const Context = React.createContext(0);
1358 function Foo() {
1359 return useContext(Context.Consumer);
1360 }
1361 ReactNoop.render(<Foo />);
1362 await waitForAll([]);
1363 assertConsoleErrorDev([
1364 'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' +
1365 'Did you mean to call useContext(Context) instead?\n' +
1366 ' in Foo (at **)',
1367 ]);
1368 });
1369
1370 // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
1371 // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
1372 // If we bailed out on referential equality, it would be confusing that you
1373 // can call this.setState(), but an autobound render callback "blocked" the update.
1374 // https://github.com/facebook/react/pull/12470#issuecomment-376917711
1375 it('does not bail out if there were no bailouts above it', async () => {
1376 const Context = React.createContext(0);
1377
1378 function Consumer({children}) {
1379 const contextValue = useContext(Context);
1380 return children(contextValue);
1381 }
1382
1383 class App extends React.Component {
1384 state = {
1385 text: 'hello',
1386 };
1387
1388 renderConsumer = context => {
1389 Scheduler.log('App#renderConsumer');
1390 return <span prop={this.state.text} />;
1391 };
1392
1393 render() {
1394 Scheduler.log('App');
1395 return (
1396 <Context.Provider value={this.props.value}>
1397 <Consumer>{this.renderConsumer}</Consumer>
1398 </Context.Provider>
1399 );
1400 }
1401 }
1402
1403 // Initial mount
1404 let inst;
1405 ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1406 await waitForAll(['App', 'App#renderConsumer']);
1407 expect(ReactNoop).toMatchRenderedOutput(<span prop="hello" />);
1408
1409 // Update
1410 inst.setState({text: 'goodbye'});
1411 await waitForAll(['App', 'App#renderConsumer']);
1412 expect(ReactNoop).toMatchRenderedOutput(<span prop="goodbye" />);
1413 });
1414 });
1415
1416 it('unwinds after errors in complete phase', async () => {
1417 const Context = React.createContext(0);
1418
1419 // This is a regression test for stack misalignment
1420 // caused by unwinding the context from wrong point.
1421 ReactNoop.render(
1422 <errorInCompletePhase>
1423 <Context.Provider value={null} />
1424 </errorInCompletePhase>,
1425 );
1426 await waitForThrow('Error in host config.');
1427
1428 ReactNoop.render(
1429 <Context.Provider value={10}>
1430 <Context.Consumer>{value => <span prop={value} />}</Context.Consumer>
1431 </Context.Provider>,
1432 );
1433 await waitForAll([]);
1434 expect(ReactNoop).toMatchRenderedOutput(<span prop={10} />);
1435 });
1436
1437 describe('fuzz test', () => {
1438 const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
1439
1440 const FLUSH_ALL = 'FLUSH_ALL';
1441 function flushAll() {
1442 return {
1443 type: FLUSH_ALL,
1444 toString() {
1445 return `flushAll()`;
1446 },
1447 };
1448 }
1449
1450 const FLUSH = 'FLUSH';
1451 function flush(unitsOfWork) {
1452 return {
1453 type: FLUSH,
1454 unitsOfWork,
1455 toString() {
1456 return `flush(${unitsOfWork})`;
1457 },
1458 };
1459 }
1460
1461 const UPDATE = 'UPDATE';
1462 function update(key, value) {
1463 return {
1464 type: UPDATE,
1465 key,
1466 value,
1467 toString() {
1468 return `update('${key}', ${value})`;
1469 },
1470 };
1471 }
1472
1473 function randomInteger(min, max) {
1474 min = Math.ceil(min);
1475 max = Math.floor(max);
1476 return Math.floor(Math.random() * (max - min)) + min;
1477 }
1478
1479 function randomAction() {
1480 switch (randomInteger(0, 3)) {
1481 case 0:
1482 return flushAll();
1483 case 1:
1484 return flush(randomInteger(0, 500));
1485 case 2:
1486 const key = contextKeys[randomInteger(0, contextKeys.length)];
1487 const value = randomInteger(1, 10);
1488 return update(key, value);
1489 default:
1490 throw new Error('Switch statement should be exhaustive');
1491 }
1492 }
1493
1494 function randomActions(n) {
1495 const actions = [];
1496 for (let i = 0; i < n; i++) {
1497 actions.push(randomAction());
1498 }
1499 return actions;
1500 }
1501
1502 function ContextSimulator(maxDepth) {
1503 const contexts = new Map(
1504 contextKeys.map(key => {
1505 const Context = React.createContext(0);
1506 Context.displayName = 'Context' + key;
1507 return [key, Context];
1508 }),
1509 );
1510
1511 class ConsumerTree extends React.Component {
1512 shouldComponentUpdate() {
1513 return false;
1514 }
1515 render() {
1516 Scheduler.log();
1517 if (this.props.depth >= this.props.maxDepth) {
1518 return null;
1519 }
1520 const consumers = [0, 1, 2].map(i => {
1521 const randomKey =
1522 contextKeys[
1523 this.props.rand.intBetween(0, contextKeys.length - 1)
1524 ];
1525 const Context = contexts.get(randomKey);
1526 return (
1527 <Context.Consumer key={i}>
1528 {value => (
1529 <>
1530 <span prop={`${randomKey}:${value}`} />
1531 <ConsumerTree
1532 rand={this.props.rand}
1533 depth={this.props.depth + 1}
1534 maxDepth={this.props.maxDepth}
1535 />
1536 </>
1537 )}
1538 </Context.Consumer>
1539 );
1540 });
1541 return consumers;
1542 }
1543 }
1544
1545 function Root(props) {
1546 return contextKeys.reduceRight(
1547 (children, key) => {
1548 const Context = contexts.get(key);
1549 const value = props.values[key];
1550 return (
1551 <Context.Provider value={value}>{children}</Context.Provider>
1552 );
1553 },
1554 <ConsumerTree
1555 rand={props.rand}
1556 depth={0}
1557 maxDepth={props.maxDepth}
1558 />,
1559 );
1560 }
1561
1562 const initialValues = contextKeys.reduce(
1563 (result, key, i) => ({...result, [key]: i + 1}),
1564 {},
1565 );
1566
1567 function assertConsistentTree(expectedValues = {}) {
1568 const jsx = ReactNoop.getChildrenAsJSX();
1569 const children = jsx === null ? [] : jsx.props.children;
1570 children.forEach(child => {
1571 const text = child.props.prop;
1572 const key = text[0];
1573 const value = parseInt(text[2], 10);
1574 const expectedValue = expectedValues[key];
1575 if (expectedValue === undefined) {
1576 // If an expected value was not explicitly passed to this function,
1577 // use the first occurrence.
1578 expectedValues[key] = value;
1579 } else if (value !== expectedValue) {
1580 throw new Error(
1581 `Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${text}`,
1582 );
1583 }
1584 });
1585 }
1586
1587 function simulate(seed, actions) {
1588 const rand = gen.create(seed);
1589 let finalExpectedValues = initialValues;
1590 function updateRoot() {
1591 ReactNoop.render(
1592 <Root
1593 maxDepth={maxDepth}
1594 rand={rand}
1595 values={finalExpectedValues}
1596 />,
1597 );
1598 }
1599 updateRoot();
1600
1601 actions.forEach(action => {
1602 switch (action.type) {
1603 case FLUSH_ALL:
1604 Scheduler.unstable_flushAllWithoutAsserting();
1605 break;
1606 case FLUSH:
1607 Scheduler.unstable_flushNumberOfYields(action.unitsOfWork);
1608 break;
1609 case UPDATE:
1610 finalExpectedValues = {
1611 ...finalExpectedValues,
1612 [action.key]: action.value,
1613 };
1614 updateRoot();
1615 break;
1616 default:
1617 throw new Error('Switch statement should be exhaustive');
1618 }
1619 assertConsistentTree();
1620 });
1621
1622 Scheduler.unstable_flushAllWithoutAsserting();
1623 assertConsistentTree(finalExpectedValues);
1624 }
1625
1626 return {simulate};
1627 }
1628
1629 it('hard-coded tests', () => {
1630 const {simulate} = ContextSimulator(5);
1631 simulate('randomSeed', [flush(3), update('A', 4)]);
1632 });
1633
1634 it('generated tests', () => {
1635 const {simulate} = ContextSimulator(5);
1636
1637 const LIMIT = 100;
1638 for (let i = 0; i < LIMIT; i++) {
1639 const seed = Math.random().toString(36).slice(2, 7);
1640 const actions = randomActions(5);
1641 try {
1642 simulate(seed, actions);
1643 } catch (error) {
1644 console.error(`
1645 Context fuzz tester error! Copy and paste the following line into the test suite:
1646 simulate('${seed}', ${actions.join(', ')});
1647 `);
1648 throw error;
1649 }
1650 }
1651 });
1652 });
1653
1654 it('should treat Context as Context.Provider', async () => {
1655 const BarContext = React.createContext({value: 'bar-initial'});
1656 expect(BarContext.Provider).toBe(BarContext);
1657
1658 function Component() {
1659 return (
1660 <BarContext value={{value: 'bar-updated'}}>
1661 <BarContext.Consumer>
1662 {({value}) => <span prop={value} />}
1663 </BarContext.Consumer>
1664 </BarContext>
1665 );
1666 }
1667
1668 ReactNoop.render(<Component />);
1669 await waitForAll([]);
1670 expect(ReactNoop).toMatchRenderedOutput(<span prop="bar-updated" />);
1671 });
1672 });