main
js 925 lines 27 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 ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 /* eslint-disable no-func-assign */
12
13 'use strict';
14
15 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
16
17 let React;
18 let ReactDOMClient;
19 let ReactDOMServer;
20 let useState;
21 let useReducer;
22 let useEffect;
23 let useContext;
24 let useCallback;
25 let useMemo;
26 let useRef;
27 let useImperativeHandle;
28 let useInsertionEffect;
29 let useLayoutEffect;
30 let useDebugValue;
31 let forwardRef;
32 let yieldedValues;
33 let yieldValue;
34 let clearLog;
35
36 function initModules() {
37 // Reset warning cache.
38 jest.resetModules();
39
40 React = require('react');
41 ReactDOMClient = require('react-dom/client');
42 ReactDOMServer = require('react-dom/server');
43 useState = React.useState;
44 useReducer = React.useReducer;
45 useEffect = React.useEffect;
46 useContext = React.useContext;
47 useCallback = React.useCallback;
48 useMemo = React.useMemo;
49 useRef = React.useRef;
50 useDebugValue = React.useDebugValue;
51 useImperativeHandle = React.useImperativeHandle;
52 useInsertionEffect = React.useInsertionEffect;
53 useLayoutEffect = React.useLayoutEffect;
54 forwardRef = React.forwardRef;
55
56 yieldedValues = [];
57 yieldValue = value => {
58 yieldedValues.push(value);
59 };
60 clearLog = () => {
61 const ret = yieldedValues;
62 yieldedValues = [];
63 return ret;
64 };
65
66 // Make them available to the helpers.
67 return {
68 ReactDOMClient,
69 ReactDOMServer,
70 };
71 }
72
73 const {
74 resetModules,
75 itRenders,
76 itThrowsWhenRendering,
77 clientRenderOnBadMarkup,
78 serverRender,
79 } = ReactDOMServerIntegrationUtils(initModules);
80
81 describe('ReactDOMServerHooks', () => {
82 beforeEach(() => {
83 resetModules();
84 });
85
86 function Text(props) {
87 yieldValue(props.text);
88 return <span>{props.text}</span>;
89 }
90
91 describe('useState', () => {
92 itRenders('basic render', async render => {
93 function Counter(props) {
94 const [count] = useState(0);
95 return <span>Count: {count}</span>;
96 }
97
98 const domNode = await render(<Counter />);
99 expect(domNode.textContent).toEqual('Count: 0');
100 });
101
102 itRenders('lazy state initialization', async render => {
103 function Counter(props) {
104 const [count] = useState(() => {
105 return 0;
106 });
107 return <span>Count: {count}</span>;
108 }
109
110 const domNode = await render(<Counter />);
111 expect(domNode.textContent).toEqual('Count: 0');
112 });
113
114 it('does not trigger a re-renders when updater is invoked outside current render function', async () => {
115 function UpdateCount({setCount, count, children}) {
116 if (count < 3) {
117 setCount(c => c + 1);
118 }
119 return <span>{children}</span>;
120 }
121 function Counter() {
122 const [count, setCount] = useState(0);
123 return (
124 <div>
125 <UpdateCount setCount={setCount} count={count}>
126 Count: {count}
127 </UpdateCount>
128 </div>
129 );
130 }
131
132 const domNode = await serverRender(<Counter />);
133 expect(domNode.textContent).toEqual('Count: 0');
134 });
135
136 itThrowsWhenRendering(
137 'if used inside a class component',
138 async render => {
139 class Counter extends React.Component {
140 render() {
141 const [count] = useState(0);
142 return <Text text={count} />;
143 }
144 }
145
146 return render(<Counter />);
147 },
148 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
149 ' one of the following reasons:\n' +
150 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
151 '2. You might be breaking the Rules of Hooks\n' +
152 '3. You might have more than one copy of React in the same app\n' +
153 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
154 );
155
156 itRenders('multiple times when an updater is called', async render => {
157 function Counter() {
158 const [count, setCount] = useState(0);
159 if (count < 12) {
160 setCount(c => c + 1);
161 setCount(c => c + 1);
162 setCount(c => c + 1);
163 }
164 return <Text text={'Count: ' + count} />;
165 }
166
167 const domNode = await render(<Counter />);
168 expect(domNode.textContent).toEqual('Count: 12');
169 });
170
171 itRenders('until there are no more new updates', async render => {
172 function Counter() {
173 const [count, setCount] = useState(0);
174 if (count < 3) {
175 setCount(count + 1);
176 }
177 return <span>Count: {count}</span>;
178 }
179
180 const domNode = await render(<Counter />);
181 expect(domNode.textContent).toEqual('Count: 3');
182 });
183
184 itThrowsWhenRendering(
185 'after too many iterations',
186 async render => {
187 function Counter() {
188 const [count, setCount] = useState(0);
189 setCount(count + 1);
190 return <span>{count}</span>;
191 }
192 return render(<Counter />);
193 },
194 'Too many re-renders. React limits the number of renders to prevent ' +
195 'an infinite loop.',
196 );
197 });
198
199 describe('useReducer', () => {
200 itRenders('with initial state', async render => {
201 function reducer(state, action) {
202 return action === 'increment' ? state + 1 : state;
203 }
204 function Counter() {
205 const [count] = useReducer(reducer, 0);
206 yieldValue('Render: ' + count);
207 return <Text text={count} />;
208 }
209
210 const domNode = await render(<Counter />);
211
212 expect(clearLog()).toEqual(['Render: 0', 0]);
213 expect(domNode.tagName).toEqual('SPAN');
214 expect(domNode.textContent).toEqual('0');
215 });
216
217 itRenders('lazy initialization', async render => {
218 function reducer(state, action) {
219 return action === 'increment' ? state + 1 : state;
220 }
221 function Counter() {
222 const [count] = useReducer(reducer, 0, c => c + 1);
223 yieldValue('Render: ' + count);
224 return <Text text={count} />;
225 }
226
227 const domNode = await render(<Counter />);
228
229 expect(clearLog()).toEqual(['Render: 1', 1]);
230 expect(domNode.tagName).toEqual('SPAN');
231 expect(domNode.textContent).toEqual('1');
232 });
233
234 itRenders(
235 'multiple times when updates happen during the render phase',
236 async render => {
237 function reducer(state, action) {
238 return action === 'increment' ? state + 1 : state;
239 }
240 function Counter() {
241 const [count, dispatch] = useReducer(reducer, 0);
242 if (count < 3) {
243 dispatch('increment');
244 }
245 yieldValue('Render: ' + count);
246 return <Text text={count} />;
247 }
248
249 const domNode = await render(<Counter />);
250
251 expect(clearLog()).toEqual([
252 'Render: 0',
253 'Render: 1',
254 'Render: 2',
255 'Render: 3',
256 3,
257 ]);
258 expect(domNode.tagName).toEqual('SPAN');
259 expect(domNode.textContent).toEqual('3');
260 },
261 );
262
263 itRenders(
264 'using reducer passed at time of render, not time of dispatch',
265 async render => {
266 // This test is a bit contrived but it demonstrates a subtle edge case.
267
268 // Reducer A increments by 1. Reducer B increments by 10.
269 function reducerA(state, action) {
270 switch (action) {
271 case 'increment':
272 return state + 1;
273 case 'reset':
274 return 0;
275 }
276 }
277 function reducerB(state, action) {
278 switch (action) {
279 case 'increment':
280 return state + 10;
281 case 'reset':
282 return 0;
283 }
284 }
285
286 function Counter() {
287 const [reducer, setReducer] = useState(() => reducerA);
288 const [count, dispatch] = useReducer(reducer, 0);
289 if (count < 20) {
290 dispatch('increment');
291 // Swap reducers each time we increment
292 if (reducer === reducerA) {
293 setReducer(() => reducerB);
294 } else {
295 setReducer(() => reducerA);
296 }
297 }
298 yieldValue('Render: ' + count);
299 return <Text text={count} />;
300 }
301
302 const domNode = await render(<Counter />);
303
304 expect(clearLog()).toEqual([
305 // The count should increase by alternating amounts of 10 and 1
306 // until we reach 21.
307 'Render: 0',
308 'Render: 10',
309 'Render: 11',
310 'Render: 21',
311 21,
312 ]);
313 expect(domNode.tagName).toEqual('SPAN');
314 expect(domNode.textContent).toEqual('21');
315 },
316 );
317 });
318
319 describe('useMemo', () => {
320 itRenders('basic render', async render => {
321 function CapitalizedText(props) {
322 const text = props.text;
323 const capitalizedText = useMemo(() => {
324 yieldValue(`Capitalize '${text}'`);
325 return text.toUpperCase();
326 }, [text]);
327 return <Text text={capitalizedText} />;
328 }
329
330 const domNode = await render(<CapitalizedText text="hello" />);
331 expect(clearLog()).toEqual(["Capitalize 'hello'", 'HELLO']);
332 expect(domNode.tagName).toEqual('SPAN');
333 expect(domNode.textContent).toEqual('HELLO');
334 });
335
336 itRenders('if no inputs are provided', async render => {
337 function LazyCompute(props) {
338 const computed = useMemo(props.compute);
339 return <Text text={computed} />;
340 }
341
342 function computeA() {
343 yieldValue('compute A');
344 return 'A';
345 }
346
347 const domNode = await render(<LazyCompute compute={computeA} />);
348 expect(clearLog()).toEqual(['compute A', 'A']);
349 expect(domNode.tagName).toEqual('SPAN');
350 expect(domNode.textContent).toEqual('A');
351 });
352
353 itRenders(
354 'multiple times when updates happen during the render phase',
355 async render => {
356 function CapitalizedText(props) {
357 const [text, setText] = useState(props.text);
358 const capitalizedText = useMemo(() => {
359 yieldValue(`Capitalize '${text}'`);
360 return text.toUpperCase();
361 }, [text]);
362
363 if (text === 'hello') {
364 setText('hello, world.');
365 }
366 return <Text text={capitalizedText} />;
367 }
368
369 const domNode = await render(<CapitalizedText text="hello" />);
370 expect(clearLog()).toEqual([
371 "Capitalize 'hello'",
372 "Capitalize 'hello, world.'",
373 'HELLO, WORLD.',
374 ]);
375 expect(domNode.tagName).toEqual('SPAN');
376 expect(domNode.textContent).toEqual('HELLO, WORLD.');
377 },
378 );
379
380 itRenders(
381 'should only invoke the memoized function when the inputs change',
382 async render => {
383 function CapitalizedText(props) {
384 const [text, setText] = useState(props.text);
385 const [count, setCount] = useState(0);
386 const capitalizedText = useMemo(() => {
387 yieldValue(`Capitalize '${text}'`);
388 return text.toUpperCase();
389 }, [text]);
390
391 yieldValue(count);
392
393 if (count < 3) {
394 setCount(count + 1);
395 }
396
397 if (text === 'hello' && count === 2) {
398 setText('hello, world.');
399 }
400 return <Text text={capitalizedText} />;
401 }
402
403 const domNode = await render(<CapitalizedText text="hello" />);
404 expect(clearLog()).toEqual([
405 "Capitalize 'hello'",
406 0,
407 1,
408 2,
409 // `capitalizedText` only recomputes when the text has changed
410 "Capitalize 'hello, world.'",
411 3,
412 'HELLO, WORLD.',
413 ]);
414 expect(domNode.tagName).toEqual('SPAN');
415 expect(domNode.textContent).toEqual('HELLO, WORLD.');
416 },
417 );
418
419 itRenders('with a warning for useState inside useMemo', async render => {
420 function App() {
421 useMemo(() => {
422 useState();
423 return 0;
424 });
425 return 'hi';
426 }
427 const domNode = await render(
428 <App />,
429 render === clientRenderOnBadMarkup
430 ? // On hydration mismatch we retry and therefore log the warning again.
431 2
432 : 1,
433 );
434 expect(domNode.textContent).toEqual('hi');
435 });
436
437 itRenders('with a warning for useRef inside useState', async render => {
438 function App() {
439 const [value] = useState(() => {
440 useRef(0);
441 return 0;
442 });
443 return value;
444 }
445
446 const domNode = await render(
447 <App />,
448 render === clientRenderOnBadMarkup
449 ? // On hydration mismatch we retry and therefore log the warning again.
450 2
451 : 1,
452 );
453 expect(domNode.textContent).toEqual('0');
454 });
455 });
456
457 describe('useRef', () => {
458 itRenders('basic render', async render => {
459 function Counter(props) {
460 const ref = useRef();
461 return <span ref={ref}>Hi</span>;
462 }
463
464 const domNode = await render(<Counter />);
465 expect(domNode.textContent).toEqual('Hi');
466 });
467
468 itRenders(
469 'multiple times when updates happen during the render phase',
470 async render => {
471 function Counter(props) {
472 const [count, setCount] = useState(0);
473 const ref = useRef();
474
475 if (count < 3) {
476 const newCount = count + 1;
477 setCount(newCount);
478 }
479
480 yieldValue(count);
481
482 return <span ref={ref}>Count: {count}</span>;
483 }
484
485 const domNode = await render(<Counter />);
486 expect(clearLog()).toEqual([0, 1, 2, 3]);
487 expect(domNode.textContent).toEqual('Count: 3');
488 },
489 );
490
491 itRenders(
492 'always return the same reference through multiple renders',
493 async render => {
494 let firstRef = null;
495 function Counter(props) {
496 const [count, setCount] = useState(0);
497 const ref = useRef();
498 if (firstRef === null) {
499 firstRef = ref;
500 } else if (firstRef !== ref) {
501 throw new Error('should never change');
502 }
503
504 if (count < 3) {
505 setCount(count + 1);
506 } else {
507 firstRef = null;
508 }
509
510 yieldValue(count);
511
512 return <span ref={ref}>Count: {count}</span>;
513 }
514
515 const domNode = await render(<Counter />);
516 expect(clearLog()).toEqual([0, 1, 2, 3]);
517 expect(domNode.textContent).toEqual('Count: 3');
518 },
519 );
520 });
521
522 describe('useEffect', () => {
523 const yields = [];
524 itRenders('should ignore effects on the server', async render => {
525 function Counter(props) {
526 useEffect(() => {
527 yieldValue('invoked on client');
528 });
529 return <Text text={'Count: ' + props.count} />;
530 }
531
532 const domNode = await render(<Counter count={0} />);
533 yields.push(clearLog());
534 expect(domNode.tagName).toEqual('SPAN');
535 expect(domNode.textContent).toEqual('Count: 0');
536 });
537
538 it('verifies yields in order', () => {
539 expect(yields).toEqual([
540 ['Count: 0'], // server render
541 ['Count: 0'], // server stream
542 ['Count: 0', 'invoked on client'], // clean render
543 ['Count: 0', 'invoked on client'], // hydrated render
544 // nothing yielded for bad markup
545 ]);
546 });
547 });
548
549 describe('useCallback', () => {
550 itRenders('should not invoke the passed callbacks', async render => {
551 function Counter(props) {
552 useCallback(() => {
553 yieldValue('should not be invoked');
554 });
555 return <Text text={'Count: ' + props.count} />;
556 }
557 const domNode = await render(<Counter count={0} />);
558 expect(clearLog()).toEqual(['Count: 0']);
559 expect(domNode.tagName).toEqual('SPAN');
560 expect(domNode.textContent).toEqual('Count: 0');
561 });
562
563 itRenders('should support render time callbacks', async render => {
564 function Counter(props) {
565 const renderCount = useCallback(increment => {
566 return 'Count: ' + (props.count + increment);
567 });
568 return <Text text={renderCount(3)} />;
569 }
570 const domNode = await render(<Counter count={2} />);
571 expect(clearLog()).toEqual(['Count: 5']);
572 expect(domNode.tagName).toEqual('SPAN');
573 expect(domNode.textContent).toEqual('Count: 5');
574 });
575
576 itRenders(
577 'should only change the returned reference when the inputs change',
578 async render => {
579 function CapitalizedText(props) {
580 const [text, setText] = useState(props.text);
581 const [count, setCount] = useState(0);
582 const capitalizeText = useCallback(() => text.toUpperCase(), [text]);
583 yieldValue(capitalizeText);
584 if (count < 3) {
585 setCount(count + 1);
586 }
587 if (text === 'hello' && count === 2) {
588 setText('hello, world.');
589 }
590 return <Text text={capitalizeText()} />;
591 }
592
593 const domNode = await render(<CapitalizedText text="hello" />);
594 const [first, second, third, fourth, result] = clearLog();
595 expect(first).toBe(second);
596 expect(second).toBe(third);
597 expect(third).not.toBe(fourth);
598 expect(result).toEqual('HELLO, WORLD.');
599 expect(domNode.tagName).toEqual('SPAN');
600 expect(domNode.textContent).toEqual('HELLO, WORLD.');
601 },
602 );
603 });
604
605 describe('useImperativeHandle', () => {
606 it('should not be invoked on the server', async () => {
607 function Counter(props, ref) {
608 useImperativeHandle(ref, () => {
609 throw new Error('should not be invoked');
610 });
611 return <Text text={props.label + ': ' + ref.current} />;
612 }
613 Counter = forwardRef(Counter);
614 const counter = React.createRef();
615 counter.current = 0;
616 const domNode = await serverRender(
617 <Counter label="Count" ref={counter} />,
618 );
619 expect(clearLog()).toEqual(['Count: 0']);
620 expect(domNode.tagName).toEqual('SPAN');
621 expect(domNode.textContent).toEqual('Count: 0');
622 });
623 });
624 describe('useInsertionEffect', () => {
625 it('should warn when invoked during render', async () => {
626 function Counter() {
627 useInsertionEffect(() => {
628 throw new Error('should not be invoked');
629 });
630
631 return <Text text="Count: 0" />;
632 }
633 const domNode = await serverRender(<Counter />, 1);
634 expect(clearLog()).toEqual(['Count: 0']);
635 expect(domNode.tagName).toEqual('SPAN');
636 expect(domNode.textContent).toEqual('Count: 0');
637 });
638 });
639
640 describe('useLayoutEffect', () => {
641 it('should warn when invoked during render', async () => {
642 function Counter() {
643 useLayoutEffect(() => {
644 throw new Error('should not be invoked');
645 });
646
647 return <Text text="Count: 0" />;
648 }
649 const domNode = await serverRender(<Counter />, 1);
650 expect(clearLog()).toEqual(['Count: 0']);
651 expect(domNode.tagName).toEqual('SPAN');
652 expect(domNode.textContent).toEqual('Count: 0');
653 });
654 });
655
656 describe('useContext', () => {
657 itThrowsWhenRendering(
658 'if used inside a class component',
659 async render => {
660 const Context = React.createContext({}, () => {});
661 class Counter extends React.Component {
662 render() {
663 const [count] = useContext(Context);
664 return <Text text={count} />;
665 }
666 }
667
668 return render(<Counter />);
669 },
670 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
671 ' one of the following reasons:\n' +
672 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
673 '2. You might be breaking the Rules of Hooks\n' +
674 '3. You might have more than one copy of React in the same app\n' +
675 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
676 );
677 });
678
679 describe('invalid hooks', () => {
680 it('warns when calling useRef inside useReducer', async () => {
681 function App() {
682 const [value, dispatch] = useReducer((state, action) => {
683 useRef(0);
684 return state + 1;
685 }, 0);
686 if (value === 0) {
687 dispatch();
688 }
689 return value;
690 }
691
692 let error;
693 try {
694 await serverRender(<App />);
695 } catch (x) {
696 error = x;
697 }
698 expect(error).not.toBe(undefined);
699 expect(error.message).toContain(
700 'Rendered more hooks than during the previous render',
701 );
702 });
703 });
704
705 itRenders(
706 'can use the same context multiple times in the same function',
707 async render => {
708 const Context = React.createContext({foo: 0, bar: 0, baz: 0});
709
710 function Provider(props) {
711 return (
712 <Context.Provider
713 value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
714 {props.children}
715 </Context.Provider>
716 );
717 }
718
719 function FooAndBar() {
720 const {foo} = useContext(Context);
721 const {bar} = useContext(Context);
722 return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
723 }
724
725 function Baz() {
726 const {baz} = useContext(Context);
727 return <Text text={'Baz: ' + baz} />;
728 }
729
730 class Indirection extends React.Component {
731 render() {
732 return this.props.children;
733 }
734 }
735
736 function App(props) {
737 return (
738 <div>
739 <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
740 <Indirection>
741 <Indirection>
742 <FooAndBar />
743 </Indirection>
744 <Indirection>
745 <Baz />
746 </Indirection>
747 </Indirection>
748 </Provider>
749 </div>
750 );
751 }
752
753 const domNode = await render(<App foo={1} bar={3} baz={5} />);
754 expect(clearLog()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
755 expect(domNode.childNodes.length).toBe(2);
756 expect(domNode.firstChild.tagName).toEqual('SPAN');
757 expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
758 expect(domNode.lastChild.tagName).toEqual('SPAN');
759 expect(domNode.lastChild.textContent).toEqual('Baz: 5');
760 },
761 );
762
763 describe('useDebugValue', () => {
764 itRenders('is a noop', async render => {
765 function Counter(props) {
766 const debugValue = useDebugValue(123);
767 return <Text text={typeof debugValue} />;
768 }
769
770 const domNode = await render(<Counter />);
771 expect(domNode.textContent).toEqual('undefined');
772 });
773 });
774
775 describe('readContext', () => {
776 function readContext(Context) {
777 const dispatcher =
778 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H;
779 return dispatcher.readContext(Context);
780 }
781
782 itRenders(
783 'can read the same context multiple times in the same function',
784 async render => {
785 const Context = React.createContext(
786 {foo: 0, bar: 0, baz: 0},
787 (a, b) => {
788 let result = 0;
789 if (a.foo !== b.foo) {
790 result |= 0b001;
791 }
792 if (a.bar !== b.bar) {
793 result |= 0b010;
794 }
795 if (a.baz !== b.baz) {
796 result |= 0b100;
797 }
798 return result;
799 },
800 );
801
802 function Provider(props) {
803 return (
804 <Context.Provider
805 value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
806 {props.children}
807 </Context.Provider>
808 );
809 }
810
811 function FooAndBar() {
812 const {foo} = readContext(Context, 0b001);
813 const {bar} = readContext(Context, 0b010);
814 return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
815 }
816
817 function Baz() {
818 const {baz} = readContext(Context, 0b100);
819 return <Text text={'Baz: ' + baz} />;
820 }
821
822 class Indirection extends React.Component {
823 shouldComponentUpdate() {
824 return false;
825 }
826 render() {
827 return this.props.children;
828 }
829 }
830
831 function App(props) {
832 return (
833 <div>
834 <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
835 <Indirection>
836 <Indirection>
837 <FooAndBar />
838 </Indirection>
839 <Indirection>
840 <Baz />
841 </Indirection>
842 </Indirection>
843 </Provider>
844 </div>
845 );
846 }
847
848 const domNode = await render(<App foo={1} bar={3} baz={5} />);
849 expect(clearLog()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
850 expect(domNode.childNodes.length).toBe(2);
851 expect(domNode.firstChild.tagName).toEqual('SPAN');
852 expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
853 expect(domNode.lastChild.tagName).toEqual('SPAN');
854 expect(domNode.lastChild.textContent).toEqual('Baz: 5');
855 },
856 );
857
858 itRenders('with a warning inside useMemo and useReducer', async render => {
859 const Context = React.createContext(42);
860
861 function ReadInMemo(props) {
862 const count = React.useMemo(() => readContext(Context), []);
863 return <Text text={count} />;
864 }
865
866 function ReadInReducer(props) {
867 const [count, dispatch] = React.useReducer(() => readContext(Context));
868 if (count !== 42) {
869 dispatch();
870 }
871 return <Text text={count} />;
872 }
873
874 const domNode1 = await render(
875 <ReadInMemo />,
876 render === clientRenderOnBadMarkup
877 ? // On hydration mismatch we retry and therefore log the warning again.
878 2
879 : 1,
880 );
881 expect(domNode1.textContent).toEqual('42');
882
883 const domNode2 = await render(<ReadInReducer />, 1);
884 expect(domNode2.textContent).toEqual('42');
885 });
886 });
887
888 it('renders successfully after a component using hooks throws an error', () => {
889 function ThrowingComponent() {
890 const [value, dispatch] = useReducer((state, action) => {
891 return state + 1;
892 }, 0);
893
894 // throw an error if the count gets too high during the re-render phase
895 if (value >= 3) {
896 throw new Error('Error from ThrowingComponent');
897 } else {
898 // dispatch to trigger a re-render of the component
899 dispatch();
900 }
901
902 return <div>{value}</div>;
903 }
904
905 function NonThrowingComponent() {
906 const [count] = useState(0);
907 return <div>{count}</div>;
908 }
909
910 // First, render a component that will throw an error during a re-render triggered
911 // by a dispatch call.
912 expect(() => ReactDOMServer.renderToString(<ThrowingComponent />)).toThrow(
913 'Error from ThrowingComponent',
914 );
915
916 // Next, assert that we can render a function component using hooks immediately
917 // after an error occurred, which indictates the internal hooks state has been
918 // reset.
919 const container = document.createElement('div');
920 container.innerHTML = ReactDOMServer.renderToString(
921 <NonThrowingComponent />,
922 );
923 expect(container.children[0].textContent).toEqual('0');
924 });
925 });