main
js 2,095 lines 63.4 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 /* eslint-disable no-func-assign */
12
13 'use strict';
14
15 let React;
16 let ReactTestRenderer;
17 let Scheduler;
18 let ReactDOMServer;
19 let act;
20 let assertLog;
21 let assertConsoleErrorDev;
22 let waitForAll;
23 let waitForThrow;
24
25 describe('ReactHooks', () => {
26 beforeEach(() => {
27 jest.resetModules();
28 React = require('react');
29 ReactTestRenderer = require('react-test-renderer');
30 Scheduler = require('scheduler');
31 ReactDOMServer = require('react-dom/server');
32 act = require('internal-test-utils').act;
33
34 const InternalTestUtils = require('internal-test-utils');
35 assertLog = InternalTestUtils.assertLog;
36 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
37 waitForAll = InternalTestUtils.waitForAll;
38 waitForThrow = InternalTestUtils.waitForThrow;
39 });
40
41 if (__DEV__) {
42 // useDebugValue is a DEV-only hook
43 it('useDebugValue throws when used in a class component', async () => {
44 class Example extends React.Component {
45 render() {
46 React.useDebugValue('abc');
47 return null;
48 }
49 }
50 await expect(async () => {
51 await act(() => {
52 ReactTestRenderer.create(<Example />, {
53 unstable_isConcurrent: true,
54 });
55 });
56 }).rejects.toThrow(
57 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' +
58 ' for one of the following reasons:\n' +
59 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
60 '2. You might be breaking the Rules of Hooks\n' +
61 '3. You might have more than one copy of React in the same app\n' +
62 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
63 );
64 });
65 }
66
67 it('bails out in the render phase if all of the state is the same', async () => {
68 const {useState, useLayoutEffect} = React;
69
70 function Child({text}) {
71 Scheduler.log('Child: ' + text);
72 return text;
73 }
74
75 let setCounter1;
76 let setCounter2;
77 function Parent() {
78 const [counter1, _setCounter1] = useState(0);
79 setCounter1 = _setCounter1;
80 const [counter2, _setCounter2] = useState(0);
81 setCounter2 = _setCounter2;
82
83 const text = `${counter1}, ${counter2}`;
84 Scheduler.log(`Parent: ${text}`);
85 useLayoutEffect(() => {
86 Scheduler.log(`Effect: ${text}`);
87 });
88 return <Child text={text} />;
89 }
90
91 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
92 root.update(<Parent />);
93 await waitForAll(['Parent: 0, 0', 'Child: 0, 0', 'Effect: 0, 0']);
94 expect(root).toMatchRenderedOutput('0, 0');
95
96 // Normal update
97 await act(() => {
98 setCounter1(1);
99 setCounter2(1);
100 });
101
102 assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']);
103
104 // Update that bails out.
105 await act(() => setCounter1(1));
106 assertLog(['Parent: 1, 1']);
107
108 // This time, one of the state updates but the other one doesn't. So we
109 // can't bail out.
110 await act(() => {
111 setCounter1(1);
112 setCounter2(2);
113 });
114
115 assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']);
116
117 // Lots of updates that eventually resolve to the current values.
118 await act(() => {
119 setCounter1(9);
120 setCounter2(3);
121 setCounter1(4);
122 setCounter2(7);
123 setCounter1(1);
124 setCounter2(2);
125 });
126
127 // Because the final values are the same as the current values, the
128 // component bails out.
129 assertLog(['Parent: 1, 2']);
130
131 // prepare to check SameValue
132 await act(() => {
133 setCounter1(0 / -1);
134 setCounter2(NaN);
135 });
136
137 assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
138
139 // check if re-setting to negative 0 / NaN still bails out
140 await act(() => {
141 setCounter1(0 / -1);
142 setCounter2(NaN);
143 setCounter2(Infinity);
144 setCounter2(NaN);
145 });
146
147 assertLog(['Parent: 0, NaN']);
148
149 // check if changing negative 0 to positive 0 does not bail out
150 await act(() => {
151 setCounter1(0);
152 });
153 assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
154 });
155
156 it('bails out in render phase if all the state is the same and props bail out with memo', async () => {
157 const {useState, memo} = React;
158
159 function Child({text}) {
160 Scheduler.log('Child: ' + text);
161 return text;
162 }
163
164 let setCounter1;
165 let setCounter2;
166 function Parent({theme}) {
167 const [counter1, _setCounter1] = useState(0);
168 setCounter1 = _setCounter1;
169 const [counter2, _setCounter2] = useState(0);
170 setCounter2 = _setCounter2;
171
172 const text = `${counter1}, ${counter2} (${theme})`;
173 Scheduler.log(`Parent: ${text}`);
174 return <Child text={text} />;
175 }
176
177 Parent = memo(Parent);
178
179 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
180 root.update(<Parent theme="light" />);
181 await waitForAll(['Parent: 0, 0 (light)', 'Child: 0, 0 (light)']);
182 expect(root).toMatchRenderedOutput('0, 0 (light)');
183
184 // Normal update
185 await act(() => {
186 setCounter1(1);
187 setCounter2(1);
188 });
189
190 assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']);
191
192 // Update that bails out.
193 await act(() => setCounter1(1));
194 assertLog(['Parent: 1, 1 (light)']);
195
196 // This time, one of the state updates but the other one doesn't. So we
197 // can't bail out.
198 await act(() => {
199 setCounter1(1);
200 setCounter2(2);
201 });
202
203 assertLog(['Parent: 1, 2 (light)', 'Child: 1, 2 (light)']);
204
205 // Updates bail out, but component still renders because props
206 // have changed
207 await act(() => {
208 setCounter1(1);
209 setCounter2(2);
210 root.update(<Parent theme="dark" />);
211 });
212
213 assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
214
215 // Both props and state bail out
216 await act(() => {
217 setCounter1(1);
218 setCounter2(2);
219 root.update(<Parent theme="dark" />);
220 });
221
222 assertLog(['Parent: 1, 2 (dark)']);
223 });
224
225 it('warns about setState second argument', async () => {
226 const {useState} = React;
227
228 let setCounter;
229 function Counter() {
230 const [counter, _setCounter] = useState(0);
231 setCounter = _setCounter;
232
233 Scheduler.log(`Count: ${counter}`);
234 return counter;
235 }
236
237 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
238 root.update(<Counter />);
239 await waitForAll(['Count: 0']);
240 expect(root).toMatchRenderedOutput('0');
241
242 await act(() =>
243 setCounter(1, () => {
244 throw new Error('Expected to ignore the callback.');
245 }),
246 );
247 assertConsoleErrorDev([
248 'State updates from the useState() and useReducer() Hooks ' +
249 "don't support the second callback argument. " +
250 'To execute a side effect after rendering, ' +
251 'declare it in the component body with useEffect().',
252 ]);
253 assertLog(['Count: 1']);
254 expect(root).toMatchRenderedOutput('1');
255 });
256
257 it('warns about dispatch second argument', async () => {
258 const {useReducer} = React;
259
260 let dispatch;
261 function Counter() {
262 const [counter, _dispatch] = useReducer((s, a) => a, 0);
263 dispatch = _dispatch;
264
265 Scheduler.log(`Count: ${counter}`);
266 return counter;
267 }
268
269 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
270 root.update(<Counter />);
271 await waitForAll(['Count: 0']);
272 expect(root).toMatchRenderedOutput('0');
273
274 await act(() =>
275 dispatch(1, () => {
276 throw new Error('Expected to ignore the callback.');
277 }),
278 );
279 assertConsoleErrorDev([
280 'State updates from the useState() and useReducer() Hooks ' +
281 "don't support the second callback argument. " +
282 'To execute a side effect after rendering, ' +
283 'declare it in the component body with useEffect().',
284 ]);
285 assertLog(['Count: 1']);
286 expect(root).toMatchRenderedOutput('1');
287 });
288
289 it('never bails out if context has changed', async () => {
290 const {useState, useLayoutEffect, useContext} = React;
291
292 const ThemeContext = React.createContext('light');
293
294 let setTheme;
295 function ThemeProvider({children}) {
296 const [theme, _setTheme] = useState('light');
297 Scheduler.log('Theme: ' + theme);
298 setTheme = _setTheme;
299 return (
300 <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
301 );
302 }
303
304 function Child({text}) {
305 Scheduler.log('Child: ' + text);
306 return text;
307 }
308
309 let setCounter;
310 function Parent() {
311 const [counter, _setCounter] = useState(0);
312 setCounter = _setCounter;
313
314 const theme = useContext(ThemeContext);
315
316 const text = `${counter} (${theme})`;
317 Scheduler.log(`Parent: ${text}`);
318 useLayoutEffect(() => {
319 Scheduler.log(`Effect: ${text}`);
320 });
321 return <Child text={text} />;
322 }
323 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
324 await act(() => {
325 root.update(
326 <ThemeProvider>
327 <Parent />
328 </ThemeProvider>,
329 );
330 });
331
332 assertLog([
333 'Theme: light',
334 'Parent: 0 (light)',
335 'Child: 0 (light)',
336 'Effect: 0 (light)',
337 ]);
338 expect(root).toMatchRenderedOutput('0 (light)');
339
340 // Updating the theme to the same value doesn't cause the consumers
341 // to re-render.
342 setTheme('light');
343 await waitForAll([]);
344 expect(root).toMatchRenderedOutput('0 (light)');
345
346 // Normal update
347 await act(() => setCounter(1));
348 assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']);
349 expect(root).toMatchRenderedOutput('1 (light)');
350
351 // Update that doesn't change state, so it bails out
352 await act(() => setCounter(1));
353 assertLog(['Parent: 1 (light)']);
354 expect(root).toMatchRenderedOutput('1 (light)');
355
356 // Update that doesn't change state, but the context changes, too, so it
357 // can't bail out
358 await act(() => {
359 setCounter(1);
360 setTheme('dark');
361 });
362
363 assertLog([
364 'Theme: dark',
365 'Parent: 1 (dark)',
366 'Child: 1 (dark)',
367 'Effect: 1 (dark)',
368 ]);
369 expect(root).toMatchRenderedOutput('1 (dark)');
370 });
371
372 it('can bail out without calling render phase (as an optimization) if queue is known to be empty', async () => {
373 const {useState, useLayoutEffect} = React;
374
375 function Child({text}) {
376 Scheduler.log('Child: ' + text);
377 return text;
378 }
379
380 let setCounter;
381 function Parent() {
382 const [counter, _setCounter] = useState(0);
383 setCounter = _setCounter;
384 Scheduler.log('Parent: ' + counter);
385 useLayoutEffect(() => {
386 Scheduler.log('Effect: ' + counter);
387 });
388 return <Child text={counter} />;
389 }
390
391 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
392 root.update(<Parent />);
393 await waitForAll(['Parent: 0', 'Child: 0', 'Effect: 0']);
394 expect(root).toMatchRenderedOutput('0');
395
396 // Normal update
397 await act(() => setCounter(1));
398 assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']);
399 expect(root).toMatchRenderedOutput('1');
400
401 // Update to the same state. React doesn't know if the queue is empty
402 // because the alternate fiber has pending update priority, so we have to
403 // enter the render phase before we can bail out. But we bail out before
404 // rendering the child, and we don't fire any effects.
405 await act(() => setCounter(1));
406 assertLog(['Parent: 1']);
407 expect(root).toMatchRenderedOutput('1');
408
409 // Update to the same state again. This times, neither fiber has pending
410 // update priority, so we can bail out before even entering the render phase.
411 await act(() => setCounter(1));
412 await waitForAll([]);
413 expect(root).toMatchRenderedOutput('1');
414
415 // This changes the state to something different so it renders normally.
416 await act(() => setCounter(2));
417 assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']);
418 expect(root).toMatchRenderedOutput('2');
419
420 // prepare to check SameValue
421 await act(() => {
422 setCounter(0);
423 });
424 assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
425 expect(root).toMatchRenderedOutput('0');
426
427 // Update to the same state for the first time to flush the queue
428 await act(() => {
429 setCounter(0);
430 });
431
432 assertLog(['Parent: 0']);
433 expect(root).toMatchRenderedOutput('0');
434
435 // Update again to the same state. Should bail out.
436 await act(() => {
437 setCounter(0);
438 });
439 await waitForAll([]);
440 expect(root).toMatchRenderedOutput('0');
441
442 // Update to a different state (positive 0 to negative 0)
443 await act(() => {
444 setCounter(0 / -1);
445 });
446 assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
447 expect(root).toMatchRenderedOutput('0');
448 });
449
450 it('bails out multiple times in a row without entering render phase', async () => {
451 const {useState} = React;
452
453 function Child({text}) {
454 Scheduler.log('Child: ' + text);
455 return text;
456 }
457
458 let setCounter;
459 function Parent() {
460 const [counter, _setCounter] = useState(0);
461 setCounter = _setCounter;
462 Scheduler.log('Parent: ' + counter);
463 return <Child text={counter} />;
464 }
465
466 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
467 root.update(<Parent />);
468 await waitForAll(['Parent: 0', 'Child: 0']);
469 expect(root).toMatchRenderedOutput('0');
470
471 const update = value => {
472 setCounter(previous => {
473 Scheduler.log(`Compute state (${previous} -> ${value})`);
474 return value;
475 });
476 };
477 ReactTestRenderer.unstable_batchedUpdates(() => {
478 update(0);
479 update(0);
480 update(0);
481 update(1);
482 update(2);
483 update(3);
484 });
485
486 assertLog([
487 // The first four updates were eagerly computed, because the queue is
488 // empty before each one.
489 'Compute state (0 -> 0)',
490 'Compute state (0 -> 0)',
491 'Compute state (0 -> 0)',
492 // The fourth update doesn't bail out
493 'Compute state (0 -> 1)',
494 // so subsequent updates can't be eagerly computed.
495 ]);
496
497 // Now let's enter the render phase
498 await waitForAll([
499 // We don't need to re-compute the first four updates. Only the final two.
500 'Compute state (1 -> 2)',
501 'Compute state (2 -> 3)',
502 'Parent: 3',
503 'Child: 3',
504 ]);
505 expect(root).toMatchRenderedOutput('3');
506 });
507
508 it('can rebase on top of a previously skipped update', async () => {
509 const {useState} = React;
510
511 function Child({text}) {
512 Scheduler.log('Child: ' + text);
513 return text;
514 }
515
516 let setCounter;
517 function Parent() {
518 const [counter, _setCounter] = useState(1);
519 setCounter = _setCounter;
520 Scheduler.log('Parent: ' + counter);
521 return <Child text={counter} />;
522 }
523
524 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
525 root.update(<Parent />);
526 await waitForAll(['Parent: 1', 'Child: 1']);
527 expect(root).toMatchRenderedOutput('1');
528
529 const update = compute => {
530 setCounter(previous => {
531 const value = compute(previous);
532 Scheduler.log(`Compute state (${previous} -> ${value})`);
533 return value;
534 });
535 };
536
537 // Update at transition priority
538 React.startTransition(() => update(n => n * 100));
539 // The new state is eagerly computed.
540 assertLog(['Compute state (1 -> 100)']);
541
542 // but before it's flushed, a higher priority update interrupts it.
543 root.unstable_flushSync(() => {
544 update(n => n + 5);
545 });
546 assertLog([
547 // The eagerly computed state was completely skipped
548 'Compute state (1 -> 6)',
549 'Parent: 6',
550 'Child: 6',
551 ]);
552 expect(root).toMatchRenderedOutput('6');
553
554 // Now when we finish the first update, the second update is rebased on top.
555 // Notice we didn't have to recompute the first update even though it was
556 // skipped in the previous render.
557 await waitForAll([
558 'Compute state (100 -> 105)',
559 'Parent: 105',
560 'Child: 105',
561 ]);
562 expect(root).toMatchRenderedOutput('105');
563 });
564
565 it('warns about variable number of dependencies', async () => {
566 const {useLayoutEffect} = React;
567 function App(props) {
568 useLayoutEffect(() => {
569 Scheduler.log('Did commit: ' + props.dependencies.join(', '));
570 }, props.dependencies);
571 return props.dependencies;
572 }
573 let root;
574 await act(() => {
575 root = ReactTestRenderer.create(<App dependencies={['A']} />, {
576 unstable_isConcurrent: true,
577 });
578 });
579 assertLog(['Did commit: A']);
580 await act(() => {
581 root.update(<App dependencies={['A', 'B']} />);
582 });
583 assertConsoleErrorDev([
584 'The final argument passed to useLayoutEffect changed size ' +
585 'between renders. The order and size of this array must remain ' +
586 'constant.\n' +
587 '\n' +
588 'Previous: [A]\n' +
589 'Incoming: [A, B]\n' +
590 ' in App (at **)',
591 ]);
592 });
593
594 it('warns if switching from dependencies to no dependencies', async () => {
595 const {useMemo} = React;
596 function App({text, hasDeps}) {
597 const resolvedText = useMemo(
598 () => {
599 Scheduler.log('Compute');
600 return text.toUpperCase();
601 },
602 hasDeps ? null : [text],
603 );
604 return resolvedText;
605 }
606
607 let root;
608 await act(() => {
609 root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
610 });
611 await act(() => {
612 root.update(<App text="Hello" hasDeps={true} />);
613 });
614 assertLog(['Compute']);
615 expect(root).toMatchRenderedOutput('HELLO');
616
617 await act(() => {
618 root.update(<App text="Hello" hasDeps={false} />);
619 });
620 assertConsoleErrorDev([
621 'useMemo received a final argument during this render, but ' +
622 'not during the previous render. Even though the final argument is ' +
623 'optional, its type cannot change between renders.\n' +
624 ' in App (at **)',
625 ]);
626 });
627
628 it('warns if deps is not an array', async () => {
629 const {useEffect, useLayoutEffect, useMemo, useCallback} = React;
630
631 function App(props) {
632 useEffect(() => {}, props.deps);
633 useLayoutEffect(() => {}, props.deps);
634 useMemo(() => {}, props.deps);
635 useCallback(() => {}, props.deps);
636 return null;
637 }
638
639 await act(() => {
640 ReactTestRenderer.create(<App deps={'hello'} />, {
641 unstable_isConcurrent: true,
642 });
643 });
644 assertConsoleErrorDev([
645 'useEffect received a final argument that is not an array (instead, received `string`). ' +
646 'When specified, the final argument must be an array.\n' +
647 ' in App (at **)',
648 'useLayoutEffect received a final argument that is not an array (instead, received `string`). ' +
649 'When specified, the final argument must be an array.\n' +
650 ' in App (at **)',
651 'useMemo received a final argument that is not an array (instead, received `string`). ' +
652 'When specified, the final argument must be an array.\n' +
653 ' in App (at **)',
654 'useCallback received a final argument that is not an array (instead, received `string`). ' +
655 'When specified, the final argument must be an array.\n' +
656 ' in App (at **)',
657 ]);
658 await act(() => {
659 ReactTestRenderer.create(<App deps={100500} />, {
660 unstable_isConcurrent: true,
661 });
662 });
663 assertConsoleErrorDev([
664 'useEffect received a final argument that is not an array (instead, received `number`). ' +
665 'When specified, the final argument must be an array.\n' +
666 ' in App (at **)',
667 'useLayoutEffect received a final argument that is not an array (instead, received `number`). ' +
668 'When specified, the final argument must be an array.\n' +
669 ' in App (at **)',
670 'useMemo received a final argument that is not an array (instead, received `number`). ' +
671 'When specified, the final argument must be an array.\n' +
672 ' in App (at **)',
673 'useCallback received a final argument that is not an array (instead, received `number`). ' +
674 'When specified, the final argument must be an array.\n' +
675 ' in App (at **)',
676 ]);
677 await act(() => {
678 ReactTestRenderer.create(<App deps={{}} />, {
679 unstable_isConcurrent: true,
680 });
681 });
682 assertConsoleErrorDev([
683 'useEffect received a final argument that is not an array (instead, received `object`). ' +
684 'When specified, the final argument must be an array.\n' +
685 ' in App (at **)',
686 'useLayoutEffect received a final argument that is not an array (instead, received `object`). ' +
687 'When specified, the final argument must be an array.\n' +
688 ' in App (at **)',
689 'useMemo received a final argument that is not an array (instead, received `object`). ' +
690 'When specified, the final argument must be an array.\n' +
691 ' in App (at **)',
692 'useCallback received a final argument that is not an array (instead, received `object`). ' +
693 'When specified, the final argument must be an array.\n' +
694 ' in App (at **)',
695 ]);
696
697 await act(() => {
698 ReactTestRenderer.create(<App deps={[]} />, {
699 unstable_isConcurrent: true,
700 });
701 ReactTestRenderer.create(<App deps={null} />, {
702 unstable_isConcurrent: true,
703 });
704 ReactTestRenderer.create(<App deps={undefined} />, {
705 unstable_isConcurrent: true,
706 });
707 });
708 });
709
710 it('warns if deps is not an array for useImperativeHandle', async () => {
711 const {useImperativeHandle} = React;
712
713 const App = React.forwardRef((props, ref) => {
714 useImperativeHandle(ref, () => {}, props.deps);
715 return null;
716 });
717 App.displayName = 'App';
718
719 await act(() => {
720 ReactTestRenderer.create(<App deps={'hello'} />, {
721 unstable_isConcurrent: true,
722 });
723 });
724 assertConsoleErrorDev([
725 'useImperativeHandle received a final argument that is not an array (instead, received `string`). ' +
726 'When specified, the final argument must be an array.\n' +
727 ' in App (at **)',
728 ]);
729 await act(() => {
730 ReactTestRenderer.create(<App deps={null} />, {
731 unstable_isConcurrent: true,
732 });
733 });
734 await act(() => {
735 ReactTestRenderer.create(<App deps={[]} />, {
736 unstable_isConcurrent: true,
737 });
738 });
739 await act(() => {
740 ReactTestRenderer.create(<App deps={undefined} />, {
741 unstable_isConcurrent: true,
742 });
743 });
744 });
745
746 it('does not forget render phase useState updates inside an effect', async () => {
747 const {useState, useEffect} = React;
748
749 function Counter() {
750 const [counter, setCounter] = useState(0);
751 if (counter === 0) {
752 setCounter(x => x + 1);
753 setCounter(x => x + 1);
754 }
755 useEffect(() => {
756 setCounter(x => x + 1);
757 setCounter(x => x + 1);
758 }, []);
759 return counter;
760 }
761
762 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
763 await act(() => {
764 root.update(<Counter />);
765 });
766 expect(root).toMatchRenderedOutput('4');
767 });
768
769 it('does not forget render phase useReducer updates inside an effect with hoisted reducer', async () => {
770 const {useReducer, useEffect} = React;
771
772 const reducer = x => x + 1;
773 function Counter() {
774 const [counter, increment] = useReducer(reducer, 0);
775 if (counter === 0) {
776 increment();
777 increment();
778 }
779 useEffect(() => {
780 increment();
781 increment();
782 }, []);
783 return counter;
784 }
785
786 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
787 await act(() => {
788 root.update(<Counter />);
789 });
790 expect(root).toMatchRenderedOutput('4');
791 });
792
793 it('does not forget render phase useReducer updates inside an effect with inline reducer', async () => {
794 const {useReducer, useEffect} = React;
795
796 function Counter() {
797 const [counter, increment] = useReducer(x => x + 1, 0);
798 if (counter === 0) {
799 increment();
800 increment();
801 }
802 useEffect(() => {
803 increment();
804 increment();
805 }, []);
806 return counter;
807 }
808
809 const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
810 await act(() => {
811 root.update(<Counter />);
812 });
813 expect(root).toMatchRenderedOutput('4');
814 });
815
816 it('warns for bad useImperativeHandle first arg', async () => {
817 const {useImperativeHandle} = React;
818 function App() {
819 useImperativeHandle({
820 focus() {},
821 });
822 return null;
823 }
824
825 await expect(async () => {
826 await act(() => {
827 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
828 });
829 }).rejects.toThrow('create is not a function');
830 assertConsoleErrorDev([
831 'Expected useImperativeHandle() second argument to be a function ' +
832 'that creates a handle. Instead received: undefined.\n' +
833 ' in App (at **)',
834 'Expected useImperativeHandle() first argument to either be a ' +
835 'ref callback or React.createRef() object. ' +
836 'Instead received: an object with keys {focus}.\n' +
837 ' in App (at **)',
838 ]);
839 });
840
841 it('warns for bad useImperativeHandle second arg', async () => {
842 const {useImperativeHandle} = React;
843 const App = React.forwardRef((props, ref) => {
844 useImperativeHandle(ref, {
845 focus() {},
846 });
847 return null;
848 });
849 App.displayName = 'App';
850
851 await act(() => {
852 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
853 });
854 assertConsoleErrorDev([
855 'Expected useImperativeHandle() second argument to be a function ' +
856 'that creates a handle. Instead received: object.\n' +
857 ' in App (at **)',
858 ]);
859 });
860
861 // https://github.com/facebook/react/issues/14022
862 it('works with ReactDOMServer calls inside a component', async () => {
863 const {useState} = React;
864 function App(props) {
865 const markup1 = ReactDOMServer.renderToString(<p>hello</p>);
866 const markup2 = ReactDOMServer.renderToStaticMarkup(<p>bye</p>);
867 const [counter] = useState(0);
868 return markup1 + counter + markup2;
869 }
870 let root;
871 await act(() => {
872 root = ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
873 });
874 expect(root.toJSON()).toMatchSnapshot();
875 });
876
877 it("throws when calling hooks inside .memo's compare function", async () => {
878 const {useState} = React;
879 function App() {
880 useState(0);
881 return null;
882 }
883 const MemoApp = React.memo(App, () => {
884 useState(0);
885 return false;
886 });
887
888 let root;
889 await act(() => {
890 root = ReactTestRenderer.create(<MemoApp />, {
891 unstable_isConcurrent: true,
892 });
893 });
894 // trying to render again should trigger comparison and throw
895 await expect(
896 act(() => {
897 root.update(<MemoApp />);
898 }),
899 ).rejects.toThrow(
900 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
901 ' one of the following reasons:\n' +
902 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
903 '2. You might be breaking the Rules of Hooks\n' +
904 '3. You might have more than one copy of React in the same app\n' +
905 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
906 );
907 // the next round, it does a fresh mount, so should render
908 await expect(
909 act(() => {
910 root.update(<MemoApp />);
911 }),
912 ).resolves.not.toThrow(
913 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
914 ' one of the following reasons:\n' +
915 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
916 '2. You might be breaking the Rules of Hooks\n' +
917 '3. You might have more than one copy of React in the same app\n' +
918 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
919 );
920 // and then again, fail
921 await expect(
922 act(() => {
923 root.update(<MemoApp />);
924 }),
925 ).rejects.toThrow(
926 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
927 ' one of the following reasons:\n' +
928 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
929 '2. You might be breaking the Rules of Hooks\n' +
930 '3. You might have more than one copy of React in the same app\n' +
931 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
932 );
933 });
934
935 it('warns when calling hooks inside useMemo', async () => {
936 const {useMemo, useState} = React;
937 function App() {
938 useMemo(() => {
939 useState(0);
940 });
941 return null;
942 }
943 await act(() => {
944 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
945 });
946 assertConsoleErrorDev([
947 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
948 'You can only call Hooks at the top level of your React function. ' +
949 'For more information, see https://react.dev/link/rules-of-hooks\n' +
950 ' in App (at **)',
951 ]);
952 });
953
954 it('warns when reading context inside useMemo', async () => {
955 const {useMemo, createContext} = React;
956 const ReactSharedInternals =
957 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
958
959 const ThemeContext = createContext('light');
960 function App() {
961 return useMemo(() => {
962 return ReactSharedInternals.H.readContext(ThemeContext);
963 }, []);
964 }
965
966 await act(() => {
967 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
968 });
969 assertConsoleErrorDev([
970 'Context can only be read while React is rendering. ' +
971 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
972 'In function components, you can read it directly in the function body, ' +
973 'but not inside Hooks like useReducer() or useMemo().\n' +
974 ' in App (at **)',
975 ]);
976 });
977
978 it('warns when reading context inside useMemo after reading outside it', async () => {
979 const {useMemo, createContext} = React;
980 const ReactSharedInternals =
981 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
982
983 const ThemeContext = createContext('light');
984 let firstRead, secondRead;
985 function App() {
986 firstRead = ReactSharedInternals.H.readContext(ThemeContext);
987 useMemo(() => {});
988 secondRead = ReactSharedInternals.H.readContext(ThemeContext);
989 return useMemo(() => {
990 return ReactSharedInternals.H.readContext(ThemeContext);
991 }, []);
992 }
993
994 await act(() => {
995 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
996 });
997 assertConsoleErrorDev([
998 'Context can only be read while React is rendering. ' +
999 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1000 'In function components, you can read it directly in the function body, ' +
1001 'but not inside Hooks like useReducer() or useMemo().\n' +
1002 ' in App (at **)',
1003 ]);
1004 expect(firstRead).toBe('light');
1005 expect(secondRead).toBe('light');
1006 });
1007
1008 // Throws because there's no runtime cost for being strict here.
1009 it('throws when reading context inside useEffect', async () => {
1010 const {useEffect, createContext} = React;
1011 const ReactSharedInternals =
1012 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1013
1014 const ThemeContext = createContext('light');
1015 function App() {
1016 useEffect(() => {
1017 ReactSharedInternals.H.readContext(ThemeContext);
1018 });
1019 return null;
1020 }
1021
1022 await act(async () => {
1023 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1024 // The exact message doesn't matter, just make sure we don't allow this
1025 await waitForThrow('Context can only be read while React is rendering');
1026 });
1027 });
1028
1029 // Throws because there's no runtime cost for being strict here.
1030 it('throws when reading context inside useLayoutEffect', async () => {
1031 const {useLayoutEffect, createContext} = React;
1032 const ReactSharedInternals =
1033 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1034
1035 const ThemeContext = createContext('light');
1036 function App() {
1037 useLayoutEffect(() => {
1038 ReactSharedInternals.H.readContext(ThemeContext);
1039 });
1040 return null;
1041 }
1042
1043 await expect(
1044 act(() => {
1045 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1046 }),
1047 ).rejects.toThrow(
1048 // The exact message doesn't matter, just make sure we don't allow this
1049 'Context can only be read while React is rendering',
1050 );
1051 });
1052
1053 it('warns when reading context inside useReducer', async () => {
1054 const {useReducer, createContext} = React;
1055 const ReactSharedInternals =
1056 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1057
1058 const ThemeContext = createContext('light');
1059 function App() {
1060 const [state, dispatch] = useReducer((s, action) => {
1061 ReactSharedInternals.H.readContext(ThemeContext);
1062 return action;
1063 }, 0);
1064 if (state === 0) {
1065 dispatch(1);
1066 }
1067 return null;
1068 }
1069
1070 await act(() => {
1071 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1072 });
1073 assertConsoleErrorDev([
1074 'Context can only be read while React is rendering. ' +
1075 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1076 'In function components, you can read it directly in the function body, ' +
1077 'but not inside Hooks like useReducer() or useMemo().\n' +
1078 ' in App (at **)',
1079 ]);
1080 });
1081
1082 // Edge case.
1083 it('warns when reading context inside eager useReducer', async () => {
1084 const {useState, createContext} = React;
1085 const ThemeContext = createContext('light');
1086
1087 const ReactSharedInternals =
1088 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1089
1090 let _setState;
1091 function Fn() {
1092 const [, setState] = useState(0);
1093 _setState = setState;
1094 return null;
1095 }
1096
1097 class Cls extends React.Component {
1098 render() {
1099 _setState(() => ReactSharedInternals.H.readContext(ThemeContext));
1100
1101 return null;
1102 }
1103 }
1104
1105 await act(() => {
1106 ReactTestRenderer.create(
1107 <>
1108 <Fn />
1109 <Cls />
1110 </>,
1111 {unstable_isConcurrent: true},
1112 );
1113 });
1114 assertConsoleErrorDev([
1115 'Context can only be read while React is rendering. ' +
1116 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1117 'In function components, you can read it directly in the function body, ' +
1118 'but not inside Hooks like useReducer() or useMemo().\n' +
1119 ' in Cls (at **)',
1120 'Cannot update a component (`Fn`) while rendering a different component (`Cls`). ' +
1121 'To locate the bad setState() call inside `Cls`, ' +
1122 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1123 ' in Cls (at **)',
1124 ]);
1125 });
1126
1127 it('warns when calling hooks inside useReducer', async () => {
1128 const {useReducer, useState, useRef} = React;
1129
1130 function App() {
1131 const [value, dispatch] = useReducer((state, action) => {
1132 useRef(0);
1133 return state + 1;
1134 }, 0);
1135 if (value === 0) {
1136 dispatch('foo');
1137 }
1138 useState();
1139 return value;
1140 }
1141
1142 await expect(async () => {
1143 await act(() => {
1144 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1145 });
1146 }).rejects.toThrow(
1147 'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
1148 );
1149 assertConsoleErrorDev([
1150 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1151 'You can only call Hooks at the top level of your React function. ' +
1152 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1153 ' in App (at **)',
1154 'React has detected a change in the order of Hooks called by App. ' +
1155 'This will lead to bugs and errors if not fixed. For more information, ' +
1156 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1157 '\n' +
1158 ' Previous render Next render\n' +
1159 ' ------------------------------------------------------\n' +
1160 '1. useReducer useReducer\n' +
1161 '2. useState useRef\n' +
1162 ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1163 '\n' +
1164 ' in App (at **)',
1165 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1166 'You can only call Hooks at the top level of your React function. ' +
1167 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1168 ' in App (at **)',
1169 ]);
1170 });
1171
1172 it("warns when calling hooks inside useState's initialize function", async () => {
1173 const {useState, useRef} = React;
1174 function App() {
1175 useState(() => {
1176 useRef(0);
1177 return 0;
1178 });
1179 return null;
1180 }
1181 await act(() => {
1182 ReactTestRenderer.create(<App />, {unstable_isConcurrent: true});
1183 });
1184 assertConsoleErrorDev([
1185 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1186 'You can only call Hooks at the top level of your React function. ' +
1187 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1188 ' in App (at **)',
1189 ]);
1190 });
1191
1192 it('resets warning internal state when interrupted by an error', async () => {
1193 const ReactSharedInternals =
1194 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1195
1196 const ThemeContext = React.createContext('light');
1197 function App() {
1198 React.useMemo(() => {
1199 // Trigger warnings
1200 ReactSharedInternals.H.readContext(ThemeContext);
1201 React.useRef();
1202 // Interrupt exit from a Hook
1203 throw new Error('No.');
1204 }, []);
1205 }
1206
1207 class Boundary extends React.Component {
1208 state = {};
1209 static getDerivedStateFromError(error) {
1210 return {err: true};
1211 }
1212 render() {
1213 if (this.state.err) {
1214 return 'Oops';
1215 }
1216 return this.props.children;
1217 }
1218 }
1219
1220 await act(() => {
1221 ReactTestRenderer.create(
1222 <Boundary>
1223 <App />
1224 </Boundary>,
1225 {unstable_isConcurrent: true},
1226 );
1227 });
1228 assertConsoleErrorDev([
1229 'Context can only be read while React is rendering. ' +
1230 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1231 'In function components, you can read it directly in the function body, ' +
1232 'but not inside Hooks like useReducer() or useMemo().\n' +
1233 ' in App (at **)',
1234 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1235 'You can only call Hooks at the top level of your React function. ' +
1236 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1237 ' in App (at **)',
1238 'Context can only be read while React is rendering. ' +
1239 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1240 'In function components, you can read it directly in the function body, ' +
1241 'but not inside Hooks like useReducer() or useMemo().\n' +
1242 ' in App (at **)',
1243 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1244 'You can only call Hooks at the top level of your React function. ' +
1245 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1246 ' in App (at **)',
1247 ]);
1248
1249 function Valid() {
1250 React.useState();
1251 React.useMemo(() => {});
1252 React.useReducer(() => {});
1253 React.useEffect(() => {});
1254 React.useLayoutEffect(() => {});
1255 React.useCallback(() => {});
1256 React.useRef();
1257 React.useImperativeHandle(
1258 () => {},
1259 () => {},
1260 );
1261 if (__DEV__) {
1262 React.useDebugValue();
1263 }
1264 return null;
1265 }
1266 // Verify it doesn't think we're still inside a Hook.
1267 // Should have no warnings.
1268 await act(() => {
1269 ReactTestRenderer.create(<Valid />, {unstable_isConcurrent: true});
1270 });
1271
1272 // Verify warnings don't get permanently disabled.
1273 await act(() => {
1274 ReactTestRenderer.create(
1275 <Boundary>
1276 <App />
1277 </Boundary>,
1278 {unstable_isConcurrent: true},
1279 );
1280 });
1281 assertConsoleErrorDev([
1282 'Context can only be read while React is rendering. ' +
1283 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1284 'In function components, you can read it directly in the function body, ' +
1285 'but not inside Hooks like useReducer() or useMemo().\n' +
1286 ' in App (at **)',
1287 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1288 'You can only call Hooks at the top level of your React function. ' +
1289 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1290 ' in App (at **)',
1291 'Context can only be read while React is rendering. ' +
1292 'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
1293 'In function components, you can read it directly in the function body, ' +
1294 'but not inside Hooks like useReducer() or useMemo().\n' +
1295 ' in App (at **)',
1296 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
1297 'You can only call Hooks at the top level of your React function. ' +
1298 'For more information, see https://react.dev/link/rules-of-hooks\n' +
1299 ' in App (at **)',
1300 ]);
1301 });
1302
1303 it('double-invokes components with Hooks in Strict Mode', async () => {
1304 const {useState, StrictMode} = React;
1305 let renderCount = 0;
1306
1307 function NoHooks() {
1308 renderCount++;
1309 return <div />;
1310 }
1311
1312 function HasHooks() {
1313 useState(0);
1314 renderCount++;
1315 return <div />;
1316 }
1317
1318 const FwdRef = React.forwardRef((props, ref) => {
1319 renderCount++;
1320 return <div />;
1321 });
1322
1323 const FwdRefHasHooks = React.forwardRef((props, ref) => {
1324 useState(0);
1325 renderCount++;
1326 return <div />;
1327 });
1328
1329 const Memo = React.memo(props => {
1330 renderCount++;
1331 return <div />;
1332 });
1333
1334 const MemoHasHooks = React.memo(props => {
1335 useState(0);
1336 renderCount++;
1337 return <div />;
1338 });
1339
1340 let renderer;
1341 await act(() => {
1342 renderer = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
1343 });
1344
1345 renderCount = 0;
1346 await act(() => {
1347 renderer.update(<NoHooks />);
1348 });
1349 expect(renderCount).toBe(1);
1350 renderCount = 0;
1351 await act(() => {
1352 renderer.update(<NoHooks />);
1353 });
1354 expect(renderCount).toBe(1);
1355 renderCount = 0;
1356 await act(() => {
1357 renderer.update(
1358 <StrictMode>
1359 <NoHooks />
1360 </StrictMode>,
1361 );
1362 });
1363 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1364 renderCount = 0;
1365 await act(() => {
1366 renderer.update(
1367 <StrictMode>
1368 <NoHooks />
1369 </StrictMode>,
1370 );
1371 });
1372 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1373
1374 renderCount = 0;
1375 await act(() => {
1376 renderer.update(<FwdRef />);
1377 });
1378 expect(renderCount).toBe(1);
1379 renderCount = 0;
1380 await act(() => {
1381 renderer.update(<FwdRef />);
1382 });
1383 expect(renderCount).toBe(1);
1384 renderCount = 0;
1385 await act(() => {
1386 renderer.update(
1387 <StrictMode>
1388 <FwdRef />
1389 </StrictMode>,
1390 );
1391 });
1392 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1393 renderCount = 0;
1394 await act(() => {
1395 renderer.update(
1396 <StrictMode>
1397 <FwdRef />
1398 </StrictMode>,
1399 );
1400 });
1401 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1402
1403 renderCount = 0;
1404 await act(() => {
1405 renderer.update(<Memo arg={1} />);
1406 });
1407 expect(renderCount).toBe(1);
1408 renderCount = 0;
1409 await act(() => {
1410 renderer.update(<Memo arg={2} />);
1411 });
1412 expect(renderCount).toBe(1);
1413 renderCount = 0;
1414 await act(() => {
1415 renderer.update(
1416 <StrictMode>
1417 <Memo arg={1} />
1418 </StrictMode>,
1419 );
1420 });
1421 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1422 renderCount = 0;
1423 await act(() => {
1424 renderer.update(
1425 <StrictMode>
1426 <Memo arg={2} />
1427 </StrictMode>,
1428 );
1429 });
1430 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1431
1432 renderCount = 0;
1433 await act(() => {
1434 renderer.update(<HasHooks />);
1435 });
1436 expect(renderCount).toBe(1);
1437 renderCount = 0;
1438 await act(() => {
1439 renderer.update(<HasHooks />);
1440 });
1441 expect(renderCount).toBe(1);
1442 renderCount = 0;
1443 await act(() => {
1444 renderer.update(
1445 <StrictMode>
1446 <HasHooks />
1447 </StrictMode>,
1448 );
1449 });
1450 expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1451 renderCount = 0;
1452 await act(() => {
1453 renderer.update(
1454 <StrictMode>
1455 <HasHooks />
1456 </StrictMode>,
1457 );
1458 });
1459 expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1460
1461 renderCount = 0;
1462 await act(() => {
1463 renderer.update(<FwdRefHasHooks />);
1464 });
1465 expect(renderCount).toBe(1);
1466 renderCount = 0;
1467 await act(() => {
1468 renderer.update(<FwdRefHasHooks />);
1469 });
1470 expect(renderCount).toBe(1);
1471 renderCount = 0;
1472 await act(() => {
1473 renderer.update(
1474 <StrictMode>
1475 <FwdRefHasHooks />
1476 </StrictMode>,
1477 );
1478 });
1479 expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1480 renderCount = 0;
1481 await act(() => {
1482 renderer.update(
1483 <StrictMode>
1484 <FwdRefHasHooks />
1485 </StrictMode>,
1486 );
1487 });
1488 expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1489
1490 renderCount = 0;
1491 await act(() => {
1492 renderer.update(<MemoHasHooks arg={1} />);
1493 });
1494 expect(renderCount).toBe(1);
1495 renderCount = 0;
1496 await act(() => {
1497 renderer.update(<MemoHasHooks arg={2} />);
1498 });
1499 expect(renderCount).toBe(1);
1500 renderCount = 0;
1501 await act(() => {
1502 renderer.update(
1503 <StrictMode>
1504 <MemoHasHooks arg={1} />
1505 </StrictMode>,
1506 );
1507 });
1508 expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1509 renderCount = 0;
1510 await act(() => {
1511 renderer.update(
1512 <StrictMode>
1513 <MemoHasHooks arg={2} />
1514 </StrictMode>,
1515 );
1516 });
1517 expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1518 });
1519
1520 it('double-invokes useMemo in DEV StrictMode despite []', async () => {
1521 const {useMemo, StrictMode} = React;
1522
1523 let useMemoCount = 0;
1524 function BadUseMemo() {
1525 useMemo(() => {
1526 useMemoCount++;
1527 }, []);
1528 return <div />;
1529 }
1530
1531 useMemoCount = 0;
1532 await act(() => {
1533 ReactTestRenderer.create(
1534 <StrictMode>
1535 <BadUseMemo />
1536 </StrictMode>,
1537 {unstable_isConcurrent: true},
1538 );
1539 });
1540 expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
1541 });
1542
1543 describe('hook ordering', () => {
1544 const useCallbackHelper = () => React.useCallback(() => {}, []);
1545 const useContextHelper = () => React.useContext(React.createContext());
1546 const useDebugValueHelper = () => React.useDebugValue('abc');
1547 const useEffectHelper = () => React.useEffect(() => () => {}, []);
1548 const useImperativeHandleHelper = () => {
1549 React.useImperativeHandle({current: null}, () => ({}), []);
1550 };
1551 const useLayoutEffectHelper = () =>
1552 React.useLayoutEffect(() => () => {}, []);
1553 const useMemoHelper = () => React.useMemo(() => 123, []);
1554 const useReducerHelper = () => React.useReducer((s, a) => a, 0);
1555 const useRefHelper = () => React.useRef(null);
1556 const useStateHelper = () => React.useState(0);
1557
1558 // We don't include useImperativeHandleHelper in this set,
1559 // because it generates an additional warning about the inputs length changing.
1560 // We test it below with its own test.
1561 const orderedHooks = [
1562 useCallbackHelper,
1563 useContextHelper,
1564 useDebugValueHelper,
1565 useEffectHelper,
1566 useLayoutEffectHelper,
1567 useMemoHelper,
1568 useReducerHelper,
1569 useRefHelper,
1570 useStateHelper,
1571 ];
1572
1573 // We don't include useContext or useDebugValue in this set,
1574 // because they aren't added to the hooks list and so won't throw.
1575 const hooksInList = [
1576 useCallbackHelper,
1577 useEffectHelper,
1578 useImperativeHandleHelper,
1579 useLayoutEffectHelper,
1580 useMemoHelper,
1581 useReducerHelper,
1582 useRefHelper,
1583 useStateHelper,
1584 ];
1585
1586 const useTransitionHelper = () => React.useTransition();
1587 const useDeferredValueHelper = () =>
1588 React.useDeferredValue(0, {timeoutMs: 1000});
1589
1590 orderedHooks.push(useTransitionHelper);
1591 orderedHooks.push(useDeferredValueHelper);
1592
1593 hooksInList.push(useTransitionHelper);
1594 hooksInList.push(useDeferredValueHelper);
1595
1596 const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => {
1597 return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${
1598 hookNameB ? `use${hookNameB}` : undefined
1599 }`;
1600 };
1601
1602 orderedHooks.forEach((firstHelper, index) => {
1603 const secondHelper =
1604 index > 0
1605 ? orderedHooks[index - 1]
1606 : orderedHooks[orderedHooks.length - 1];
1607
1608 const hookNameA = firstHelper.name
1609 .replace('use', '')
1610 .replace('Helper', '');
1611 const hookNameB = secondHelper.name
1612 .replace('use', '')
1613 .replace('Helper', '');
1614
1615 it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, async () => {
1616 function App(props) {
1617 if (props.update) {
1618 secondHelper();
1619 firstHelper();
1620 } else {
1621 firstHelper();
1622 secondHelper();
1623 }
1624 // This should not appear in the warning message because it occurs after the first mismatch
1625 useRefHelper();
1626 return null;
1627 }
1628 let root;
1629 await act(() => {
1630 root = ReactTestRenderer.create(<App update={false} />, {
1631 unstable_isConcurrent: true,
1632 });
1633 });
1634 try {
1635 await act(() => {
1636 root.update(<App update={true} />);
1637 });
1638 } catch (error) {
1639 // Swapping certain types of hooks will cause runtime errors.
1640 // This is okay as far as this test is concerned.
1641 // We just want to verify that warnings are always logged.
1642 }
1643 assertConsoleErrorDev([
1644 'React has detected a change in the order of Hooks called by App. ' +
1645 'This will lead to bugs and errors if not fixed. For more information, ' +
1646 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1647 '\n' +
1648 ' Previous render Next render\n' +
1649 ' ------------------------------------------------------\n' +
1650 `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` +
1651 ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1652 '\n' +
1653 ' in App (at **)',
1654 ]);
1655
1656 // further warnings for this component are silenced
1657 try {
1658 await act(() => {
1659 root.update(<App update={false} />);
1660 });
1661 } catch (error) {
1662 // Swapping certain types of hooks will cause runtime errors.
1663 // This is okay as far as this test is concerned.
1664 // We just want to verify that warnings are always logged.
1665 }
1666 });
1667
1668 it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
1669 function App(props) {
1670 if (props.update) {
1671 firstHelper();
1672 secondHelper();
1673 } else {
1674 firstHelper();
1675 }
1676 return null;
1677 }
1678 let root;
1679 await act(() => {
1680 root = ReactTestRenderer.create(<App update={false} />, {
1681 unstable_isConcurrent: true,
1682 });
1683 });
1684
1685 try {
1686 await act(() => {
1687 root.update(<App update={true} />);
1688 });
1689 } catch (error) {
1690 // Swapping certain types of hooks will cause runtime errors.
1691 // This is okay as far as this test is concerned.
1692 // We just want to verify that warnings are always logged.
1693 }
1694 assertConsoleErrorDev([
1695 'React has detected a change in the order of Hooks called by App. ' +
1696 'This will lead to bugs and errors if not fixed. For more information, ' +
1697 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1698 '\n' +
1699 ' Previous render Next render\n' +
1700 ' ------------------------------------------------------\n' +
1701 `1. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` +
1702 `2. undefined use${hookNameB}\n` +
1703 ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1704 '\n' +
1705 ' in App (at **)',
1706 ]);
1707 });
1708 });
1709
1710 hooksInList.forEach((firstHelper, index) => {
1711 const secondHelper =
1712 index > 0
1713 ? hooksInList[index - 1]
1714 : hooksInList[hooksInList.length - 1];
1715
1716 const hookNameA = firstHelper.name
1717 .replace('use', '')
1718 .replace('Helper', '');
1719 const hookNameB = secondHelper.name
1720 .replace('use', '')
1721 .replace('Helper', '');
1722
1723 it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
1724 function App(props) {
1725 if (props.update) {
1726 firstHelper();
1727 } else {
1728 firstHelper();
1729 secondHelper();
1730 }
1731 return null;
1732 }
1733 let root;
1734 await act(() => {
1735 root = ReactTestRenderer.create(<App update={false} />, {
1736 unstable_isConcurrent: true,
1737 });
1738 });
1739
1740 await expect(async () => {
1741 await act(() => {
1742 root.update(<App update={true} />);
1743 });
1744 }).rejects.toThrow('Rendered fewer hooks than expected. ');
1745 });
1746 });
1747
1748 it(
1749 'warns on using differently ordered hooks ' +
1750 '(useImperativeHandleHelper, useMemoHelper) on subsequent renders',
1751 async () => {
1752 function App(props) {
1753 if (props.update) {
1754 useMemoHelper();
1755 useImperativeHandleHelper();
1756 } else {
1757 useImperativeHandleHelper();
1758 useMemoHelper();
1759 }
1760 // This should not appear in the warning message because it occurs after the first mismatch
1761 useRefHelper();
1762 return null;
1763 }
1764 let root;
1765 await act(() => {
1766 root = ReactTestRenderer.create(<App update={false} />, {
1767 unstable_isConcurrent: true,
1768 });
1769 });
1770 await act(() => {
1771 root.update(<App update={true} />);
1772 }).catch(e => {});
1773 // Swapping certain types of hooks will cause runtime errors.
1774 // This is okay as far as this test is concerned.
1775 // We just want to verify that warnings are always logged.
1776 assertConsoleErrorDev([
1777 'React has detected a change in the order of Hooks called by App. ' +
1778 'This will lead to bugs and errors if not fixed. For more information, ' +
1779 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1780 '\n' +
1781 ' Previous render Next render\n' +
1782 ' ------------------------------------------------------\n' +
1783 `1. ${formatHookNamesToMatchErrorMessage(
1784 'ImperativeHandle',
1785 'Memo',
1786 )}\n` +
1787 ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1788 '\n' +
1789 ' in App (at **)',
1790 ]);
1791
1792 // further warnings for this component are silenced
1793 await act(() => {
1794 root.update(<App update={false} />);
1795 });
1796 },
1797 );
1798
1799 it('detects a bad hook order even if the component throws', async () => {
1800 const {useState, useReducer} = React;
1801 function useCustomHook() {
1802 useState(0);
1803 }
1804 function App(props) {
1805 if (props.update) {
1806 useCustomHook();
1807 useReducer((s, a) => a, 0);
1808 throw new Error('custom error');
1809 } else {
1810 useReducer((s, a) => a, 0);
1811 useCustomHook();
1812 }
1813 return null;
1814 }
1815 let root;
1816 await act(() => {
1817 root = ReactTestRenderer.create(<App update={false} />, {
1818 unstable_isConcurrent: true,
1819 });
1820 });
1821 await expect(async () => {
1822 await act(() => {
1823 root.update(<App update={true} />);
1824 });
1825 }).rejects.toThrow('custom error');
1826 assertConsoleErrorDev([
1827 'React has detected a change in the order of Hooks called by App. ' +
1828 'This will lead to bugs and errors if not fixed. For more information, ' +
1829 'read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n' +
1830 '\n' +
1831 ' Previous render Next render\n' +
1832 ' ------------------------------------------------------\n' +
1833 '1. useReducer useState\n' +
1834 ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' +
1835 '\n' +
1836 ' in App (at **)',
1837 ]);
1838 });
1839 });
1840
1841 // Regression test for #14674
1842 it('does not swallow original error when updating another component in render phase', async () => {
1843 const {useState} = React;
1844
1845 let _setState;
1846 function A() {
1847 const [, setState] = useState(0);
1848 _setState = setState;
1849 return null;
1850 }
1851
1852 function B() {
1853 _setState(() => {
1854 throw new Error('Hello');
1855 });
1856 return null;
1857 }
1858
1859 await expect(async () => {
1860 await act(() => {
1861 ReactTestRenderer.create(
1862 <>
1863 <A />
1864 <B />
1865 </>,
1866 {unstable_isConcurrent: true},
1867 );
1868 });
1869 }).rejects.toThrow('Hello');
1870 assertConsoleErrorDev([
1871 'Cannot update a component (`A`) while rendering a different component (`B`). ' +
1872 'To locate the bad setState() call inside `B`, ' +
1873 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
1874 ' in B (at **)',
1875 ]);
1876 });
1877
1878 // Regression test for https://github.com/facebook/react/issues/15057
1879 it('does not fire a false positive warning when previous effect unmounts the component', async () => {
1880 const {useState, useEffect} = React;
1881 let globalListener;
1882
1883 function A() {
1884 const [show, setShow] = useState(true);
1885 function hideMe() {
1886 setShow(false);
1887 }
1888 return show ? <B hideMe={hideMe} /> : null;
1889 }
1890
1891 function B(props) {
1892 return <C {...props} />;
1893 }
1894
1895 function C({hideMe}) {
1896 const [, setState] = useState();
1897
1898 useEffect(() => {
1899 let isStale = false;
1900
1901 globalListener = () => {
1902 if (!isStale) {
1903 setState('hello');
1904 }
1905 };
1906
1907 return () => {
1908 isStale = true;
1909 hideMe();
1910 };
1911 });
1912 return null;
1913 }
1914
1915 await act(() => {
1916 ReactTestRenderer.create(<A />, {unstable_isConcurrent: true});
1917 });
1918
1919 // Note: should *not* warn about updates on unmounted component.
1920 // Because there's no way for component to know it got unmounted.
1921 await expect(
1922 act(() => {
1923 globalListener();
1924 globalListener();
1925 }),
1926 ).resolves.not.toThrow();
1927 });
1928
1929 // Regression test for https://github.com/facebook/react/issues/14790
1930 it('does not fire a false positive warning when suspending memo', async () => {
1931 const {Suspense, useState} = React;
1932
1933 let isSuspended = true;
1934 let resolve;
1935 function trySuspend() {
1936 if (isSuspended) {
1937 throw new Promise(res => {
1938 resolve = () => {
1939 isSuspended = false;
1940 res();
1941 };
1942 });
1943 }
1944 }
1945
1946 function Child() {
1947 useState();
1948 trySuspend();
1949 return 'hello';
1950 }
1951
1952 const Wrapper = React.memo(Child);
1953 let root;
1954 await act(() => {
1955 root = ReactTestRenderer.create(
1956 <Suspense fallback="loading">
1957 <Wrapper />
1958 </Suspense>,
1959 {unstable_isConcurrent: true},
1960 );
1961 });
1962 expect(root).toMatchRenderedOutput('loading');
1963 await act(resolve);
1964 expect(root).toMatchRenderedOutput('hello');
1965 });
1966
1967 // Regression test for https://github.com/facebook/react/issues/14790
1968 it('does not fire a false positive warning when suspending forwardRef', async () => {
1969 const {Suspense, useState} = React;
1970
1971 let isSuspended = true;
1972 let resolve;
1973 function trySuspend() {
1974 if (isSuspended) {
1975 throw new Promise(res => {
1976 resolve = () => {
1977 isSuspended = false;
1978 res();
1979 };
1980 });
1981 }
1982 }
1983
1984 function render(props, ref) {
1985 useState();
1986 trySuspend();
1987 return 'hello';
1988 }
1989
1990 const Wrapper = React.forwardRef(render);
1991 let root;
1992 await act(() => {
1993 root = ReactTestRenderer.create(
1994 <Suspense fallback="loading">
1995 <Wrapper />
1996 </Suspense>,
1997 {unstable_isConcurrent: true},
1998 );
1999 });
2000 expect(root).toMatchRenderedOutput('loading');
2001 await act(resolve);
2002 expect(root).toMatchRenderedOutput('hello');
2003 });
2004
2005 // Regression test for https://github.com/facebook/react/issues/14790
2006 it('does not fire a false positive warning when suspending memo(forwardRef)', async () => {
2007 const {Suspense, useState} = React;
2008
2009 let isSuspended = true;
2010 let resolve;
2011 function trySuspend() {
2012 if (isSuspended) {
2013 throw new Promise(res => {
2014 resolve = () => {
2015 isSuspended = false;
2016 res();
2017 };
2018 });
2019 }
2020 }
2021
2022 function render(props, ref) {
2023 useState();
2024 trySuspend();
2025 return 'hello';
2026 }
2027
2028 const Wrapper = React.memo(React.forwardRef(render));
2029 let root;
2030 await act(() => {
2031 root = ReactTestRenderer.create(
2032 <Suspense fallback="loading">
2033 <Wrapper />
2034 </Suspense>,
2035 {unstable_isConcurrent: true},
2036 );
2037 });
2038 expect(root).toMatchRenderedOutput('loading');
2039 await act(resolve);
2040 expect(root).toMatchRenderedOutput('hello');
2041 });
2042
2043 // Regression test for https://github.com/facebook/react/issues/15732
2044 it('resets hooks when an error is thrown in the middle of a list of hooks', async () => {
2045 const {useEffect, useState} = React;
2046
2047 class ErrorBoundary extends React.Component {
2048 state = {hasError: false};
2049
2050 static getDerivedStateFromError() {
2051 return {hasError: true};
2052 }
2053
2054 render() {
2055 return (
2056 <Wrapper>
2057 {this.state.hasError ? 'Error!' : this.props.children}
2058 </Wrapper>
2059 );
2060 }
2061 }
2062
2063 function Wrapper({children}) {
2064 return children;
2065 }
2066
2067 let setShouldThrow;
2068 function Thrower() {
2069 const [shouldThrow, _setShouldThrow] = useState(false);
2070 setShouldThrow = _setShouldThrow;
2071
2072 if (shouldThrow) {
2073 throw new Error('Throw!');
2074 }
2075
2076 useEffect(() => {}, []);
2077
2078 return 'Throw!';
2079 }
2080
2081 let root;
2082 await act(() => {
2083 root = ReactTestRenderer.create(
2084 <ErrorBoundary>
2085 <Thrower />
2086 </ErrorBoundary>,
2087 {unstable_isConcurrent: true},
2088 );
2089 });
2090
2091 expect(root).toMatchRenderedOutput('Throw!');
2092 await act(() => setShouldThrow(true));
2093 expect(root).toMatchRenderedOutput('Error!');
2094 });
2095 });