main
js 2,285 lines 63.7 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 * @jest-environment node
8 */
9
10 'use strict';
11
12 const ESLintTesterV7 = require('eslint-v7').RuleTester;
13 const ESLintTesterV9 = require('eslint-v9').RuleTester;
14 const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');
15 const ReactHooksESLintRule =
16 ReactHooksESLintPlugin.default.rules['rules-of-hooks'];
17
18 /**
19 * A string template tag that removes padding from the left side of multi-line strings
20 * @param {Array} strings array of code strings (only one expected)
21 */
22 function normalizeIndent(strings) {
23 const codeLines = strings[0].split('\n');
24 const leftPadding = codeLines[1].match(/\s+/)[0];
25 return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
26 }
27
28 // ***************************************************
29 // For easier local testing, you can add to any case:
30 // {
31 // skip: true,
32 // --or--
33 // only: true,
34 // ...
35 // }
36 // ***************************************************
37
38 const allTests = {
39 valid: [
40 {
41 code: normalizeIndent`
42 // Valid because components can use hooks.
43 function ComponentWithHook() {
44 useHook();
45 }
46 `,
47 },
48 {
49 syntax: 'flow',
50 code: normalizeIndent`
51 // Component syntax
52 component Button() {
53 useHook();
54 return <div>Button!</div>;
55 }
56 `,
57 },
58 {
59 syntax: 'flow',
60 code: normalizeIndent`
61 // Hook syntax
62 hook useSampleHook() {
63 useHook();
64 }
65 `,
66 },
67 {
68 code: normalizeIndent`
69 // Valid because components can use hooks.
70 function createComponentWithHook() {
71 return function ComponentWithHook() {
72 useHook();
73 };
74 }
75 `,
76 },
77 {
78 code: normalizeIndent`
79 // Valid because hooks can use hooks.
80 function useHookWithHook() {
81 useHook();
82 }
83 `,
84 },
85 {
86 code: normalizeIndent`
87 // Valid because hooks can use hooks.
88 function createHook() {
89 return function useHookWithHook() {
90 useHook();
91 }
92 }
93 `,
94 },
95 {
96 code: normalizeIndent`
97 // Valid because components can call functions.
98 function ComponentWithNormalFunction() {
99 doSomething();
100 }
101 `,
102 },
103 {
104 code: normalizeIndent`
105 // Valid because functions can call functions.
106 function normalFunctionWithNormalFunction() {
107 doSomething();
108 }
109 `,
110 },
111 {
112 code: normalizeIndent`
113 // Valid because functions can call functions.
114 function normalFunctionWithConditionalFunction() {
115 if (cond) {
116 doSomething();
117 }
118 }
119 `,
120 },
121 {
122 code: normalizeIndent`
123 // Valid because functions can call functions.
124 function functionThatStartsWithUseButIsntAHook() {
125 if (cond) {
126 userFetch();
127 }
128 }
129 `,
130 },
131 {
132 code: normalizeIndent`
133 // Valid although unconditional return doesn't make sense and would fail other rules.
134 // We could make it invalid but it doesn't matter.
135 function useUnreachable() {
136 return;
137 useHook();
138 }
139 `,
140 },
141 {
142 code: normalizeIndent`
143 // Valid because hooks can call hooks.
144 function useHook() { useState(); }
145 const whatever = function useHook() { useState(); };
146 const useHook1 = () => { useState(); };
147 let useHook2 = () => useState();
148 useHook2 = () => { useState(); };
149 ({useHook: () => { useState(); }});
150 ({useHook() { useState(); }});
151 const {useHook3 = () => { useState(); }} = {};
152 ({useHook = () => { useState(); }} = {});
153 Namespace.useHook = () => { useState(); };
154 `,
155 },
156 {
157 code: normalizeIndent`
158 // Valid because hooks can call hooks.
159 function useHook() {
160 useHook1();
161 useHook2();
162 }
163 `,
164 },
165 {
166 code: normalizeIndent`
167 // Valid because hooks can call hooks.
168 function createHook() {
169 return function useHook() {
170 useHook1();
171 useHook2();
172 };
173 }
174 `,
175 },
176 {
177 code: normalizeIndent`
178 // Valid because hooks can call hooks.
179 function useHook() {
180 useState() && a;
181 }
182 `,
183 },
184 {
185 code: normalizeIndent`
186 // Valid because hooks can call hooks.
187 function useHook() {
188 return useHook1() + useHook2();
189 }
190 `,
191 },
192 {
193 code: normalizeIndent`
194 // Valid because hooks can call hooks.
195 function useHook() {
196 return useHook1(useHook2());
197 }
198 `,
199 },
200 {
201 code: normalizeIndent`
202 // Valid because hooks can be used in anonymous arrow-function arguments
203 // to forwardRef.
204 const FancyButton = React.forwardRef((props, ref) => {
205 useHook();
206 return <button {...props} ref={ref} />
207 });
208 `,
209 },
210 {
211 code: normalizeIndent`
212 // Valid because hooks can be used in anonymous function arguments to
213 // forwardRef.
214 const FancyButton = React.forwardRef(function (props, ref) {
215 useHook();
216 return <button {...props} ref={ref} />
217 });
218 `,
219 },
220 {
221 code: normalizeIndent`
222 // Valid because hooks can be used in anonymous function arguments to
223 // forwardRef.
224 const FancyButton = forwardRef(function (props, ref) {
225 useHook();
226 return <button {...props} ref={ref} />
227 });
228 `,
229 },
230 {
231 code: normalizeIndent`
232 // Valid because hooks can be used in anonymous function arguments to
233 // React.memo.
234 const MemoizedFunction = React.memo(props => {
235 useHook();
236 return <button {...props} />
237 });
238 `,
239 },
240 {
241 code: normalizeIndent`
242 // Valid because hooks can be used in anonymous function arguments to
243 // memo.
244 const MemoizedFunction = memo(function (props) {
245 useHook();
246 return <button {...props} />
247 });
248 `,
249 },
250 {
251 code: normalizeIndent`
252 // Valid because classes can call functions.
253 // We don't consider these to be hooks.
254 class C {
255 m() {
256 this.useHook();
257 super.useHook();
258 }
259 }
260 `,
261 },
262 {
263 code: normalizeIndent`
264 // Valid -- this is a regression test.
265 jest.useFakeTimers();
266 beforeEach(() => {
267 jest.useRealTimers();
268 })
269 `,
270 },
271 {
272 code: normalizeIndent`
273 // Valid because they're not matching use[A-Z].
274 fooState();
275 _use();
276 _useState();
277 use_hook();
278 // also valid because it's not matching the PascalCase namespace
279 jest.useFakeTimer()
280 `,
281 },
282 {
283 code: normalizeIndent`
284 // Regression test for some internal code.
285 // This shows how the "callback rule" is more relaxed,
286 // and doesn't kick in unless we're confident we're in
287 // a component or a hook.
288 function makeListener(instance) {
289 each(pixelsWithInferredEvents, pixel => {
290 if (useExtendedSelector(pixel.id) && extendedButton) {
291 foo();
292 }
293 });
294 }
295 `,
296 },
297 {
298 code: normalizeIndent`
299 // This is valid because "use"-prefixed functions called in
300 // unnamed function arguments are not assumed to be hooks.
301 React.unknownFunction((foo, bar) => {
302 if (foo) {
303 useNotAHook(bar)
304 }
305 });
306 `,
307 },
308 {
309 code: normalizeIndent`
310 // This is valid because "use"-prefixed functions called in
311 // unnamed function arguments are not assumed to be hooks.
312 unknownFunction(function(foo, bar) {
313 if (foo) {
314 useNotAHook(bar)
315 }
316 });
317 `,
318 },
319 {
320 code: normalizeIndent`
321 // Regression test for incorrectly flagged valid code.
322 function RegressionTest() {
323 const foo = cond ? a : b;
324 useState();
325 }
326 `,
327 },
328 {
329 code: normalizeIndent`
330 // Valid because exceptions abort rendering
331 function RegressionTest() {
332 if (page == null) {
333 throw new Error('oh no!');
334 }
335 useState();
336 }
337 `,
338 },
339 {
340 code: normalizeIndent`
341 // Valid because the loop doesn't change the order of hooks calls.
342 function RegressionTest() {
343 const res = [];
344 const additionalCond = true;
345 for (let i = 0; i !== 10 && additionalCond; ++i ) {
346 res.push(i);
347 }
348 React.useLayoutEffect(() => {});
349 }
350 `,
351 },
352 {
353 code: normalizeIndent`
354 // Is valid but hard to compute by brute-forcing
355 function MyComponent() {
356 // 40 conditions
357 if (c) {} else {}
358 if (c) {} else {}
359 if (c) {} else {}
360 if (c) {} else {}
361 if (c) {} else {}
362 if (c) {} else {}
363 if (c) {} else {}
364 if (c) {} else {}
365 if (c) {} else {}
366 if (c) {} else {}
367 if (c) {} else {}
368 if (c) {} else {}
369 if (c) {} else {}
370 if (c) {} else {}
371 if (c) {} else {}
372 if (c) {} else {}
373 if (c) {} else {}
374 if (c) {} else {}
375 if (c) {} else {}
376 if (c) {} else {}
377 if (c) {} else {}
378 if (c) {} else {}
379 if (c) {} else {}
380 if (c) {} else {}
381 if (c) {} else {}
382 if (c) {} else {}
383 if (c) {} else {}
384 if (c) {} else {}
385 if (c) {} else {}
386 if (c) {} else {}
387 if (c) {} else {}
388 if (c) {} else {}
389 if (c) {} else {}
390 if (c) {} else {}
391 if (c) {} else {}
392 if (c) {} else {}
393 if (c) {} else {}
394 if (c) {} else {}
395 if (c) {} else {}
396 if (c) {} else {}
397
398 // 10 hooks
399 useHook();
400 useHook();
401 useHook();
402 useHook();
403 useHook();
404 useHook();
405 useHook();
406 useHook();
407 useHook();
408 useHook();
409 }
410 `,
411 },
412 {
413 code: normalizeIndent`
414 // Valid because the neither the conditions before or after the hook affect the hook call
415 // Failed prior to implementing BigInt because pathsFromStartToEnd and allPathsFromStartToEnd were too big and had rounding errors
416 const useSomeHook = () => {};
417
418 const SomeName = () => {
419 const filler = FILLER ?? FILLER ?? FILLER;
420 const filler2 = FILLER ?? FILLER ?? FILLER;
421 const filler3 = FILLER ?? FILLER ?? FILLER;
422 const filler4 = FILLER ?? FILLER ?? FILLER;
423 const filler5 = FILLER ?? FILLER ?? FILLER;
424 const filler6 = FILLER ?? FILLER ?? FILLER;
425 const filler7 = FILLER ?? FILLER ?? FILLER;
426 const filler8 = FILLER ?? FILLER ?? FILLER;
427
428 useSomeHook();
429
430 if (anyConditionCanEvenBeFalse) {
431 return null;
432 }
433
434 return (
435 <React.Fragment>
436 {FILLER ? FILLER : FILLER}
437 {FILLER ? FILLER : FILLER}
438 {FILLER ? FILLER : FILLER}
439 {FILLER ? FILLER : FILLER}
440 {FILLER ? FILLER : FILLER}
441 {FILLER ? FILLER : FILLER}
442 {FILLER ? FILLER : FILLER}
443 {FILLER ? FILLER : FILLER}
444 {FILLER ? FILLER : FILLER}
445 {FILLER ? FILLER : FILLER}
446 {FILLER ? FILLER : FILLER}
447 {FILLER ? FILLER : FILLER}
448 {FILLER ? FILLER : FILLER}
449 {FILLER ? FILLER : FILLER}
450 {FILLER ? FILLER : FILLER}
451 {FILLER ? FILLER : FILLER}
452 {FILLER ? FILLER : FILLER}
453 {FILLER ? FILLER : FILLER}
454 {FILLER ? FILLER : FILLER}
455 {FILLER ? FILLER : FILLER}
456 {FILLER ? FILLER : FILLER}
457 {FILLER ? FILLER : FILLER}
458 {FILLER ? FILLER : FILLER}
459 {FILLER ? FILLER : FILLER}
460 {FILLER ? FILLER : FILLER}
461 {FILLER ? FILLER : FILLER}
462 {FILLER ? FILLER : FILLER}
463 {FILLER ? FILLER : FILLER}
464 {FILLER ? FILLER : FILLER}
465 {FILLER ? FILLER : FILLER}
466 {FILLER ? FILLER : FILLER}
467 {FILLER ? FILLER : FILLER}
468 {FILLER ? FILLER : FILLER}
469 {FILLER ? FILLER : FILLER}
470 {FILLER ? FILLER : FILLER}
471 {FILLER ? FILLER : FILLER}
472 {FILLER ? FILLER : FILLER}
473 {FILLER ? FILLER : FILLER}
474 {FILLER ? FILLER : FILLER}
475 {FILLER ? FILLER : FILLER}
476 {FILLER ? FILLER : FILLER}
477 {FILLER ? FILLER : FILLER}
478 </React.Fragment>
479 );
480 };
481 `,
482 },
483 {
484 code: normalizeIndent`
485 // Valid because the neither the condition nor the loop affect the hook call.
486 function App(props) {
487 const someObject = {propA: true};
488 for (const propName in someObject) {
489 if (propName === true) {
490 } else {
491 }
492 }
493 const [myState, setMyState] = useState(null);
494 }
495 `,
496 },
497 {
498 code: normalizeIndent`
499 function App() {
500 const text = use(Promise.resolve('A'));
501 return <Text text={text} />
502 }
503 `,
504 },
505 {
506 code: normalizeIndent`
507 import * as React from 'react';
508 function App() {
509 if (shouldShowText) {
510 const text = use(query);
511 const data = React.use(thing);
512 const data2 = react.use(thing2);
513 return <Text text={text} />
514 }
515 return <Text text={shouldFetchBackupText ? use(backupQuery) : "Nothing to see here"} />
516 }
517 `,
518 },
519 {
520 code: normalizeIndent`
521 function App() {
522 let data = [];
523 for (const query of queries) {
524 const text = use(item);
525 data.push(text);
526 }
527 return <Child data={data} />
528 }
529 `,
530 },
531 {
532 code: normalizeIndent`
533 function App() {
534 const data = someCallback((x) => use(x));
535 return <Child data={data} />
536 }
537 `,
538 },
539 {
540 code: normalizeIndent`
541 export const notAComponent = () => {
542 return () => {
543 useState();
544 }
545 }
546 `,
547 // TODO: this should error but doesn't.
548 // errors: [functionError('use', 'notAComponent')],
549 },
550 {
551 code: normalizeIndent`
552 export default () => {
553 if (isVal) {
554 useState(0);
555 }
556 }
557 `,
558 // TODO: this should error but doesn't.
559 // errors: [genericError('useState')],
560 },
561 {
562 code: normalizeIndent`
563 function notAComponent() {
564 return new Promise.then(() => {
565 useState();
566 });
567 }
568 `,
569 // TODO: this should error but doesn't.
570 // errors: [genericError('useState')],
571 },
572 {
573 code: normalizeIndent`
574 // Valid because the hook is outside of the loop
575 const Component = () => {
576 const [state, setState] = useState(0);
577 for (let i = 0; i < 10; i++) {
578 console.log(i);
579 }
580 return <div></div>;
581 };
582 `,
583 },
584 {
585 code: normalizeIndent`
586 // Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
587 function MyComponent({ theme }) {
588 const onClick = useEffectEvent(() => {
589 showNotification(theme);
590 });
591 useMyEffect(() => {
592 onClick();
593 });
594 useServerEffect(() => {
595 onClick();
596 });
597 }
598 `,
599 settings: {
600 'react-hooks': {
601 additionalEffectHooks: '(useMyEffect|useServerEffect)',
602 },
603 },
604 },
605 {
606 syntax: 'flow',
607 code: normalizeIndent`
608 // Component syntax version
609 // Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
610 component MyComponent(theme: any) {
611 const onClick = useEffectEvent(() => {
612 showNotification(theme);
613 });
614 useMyEffect(() => {
615 onClick();
616 });
617 useServerEffect(() => {
618 onClick();
619 });
620 }
621 `,
622 settings: {
623 'react-hooks': {
624 additionalEffectHooks: '(useMyEffect|useServerEffect)',
625 },
626 },
627 },
628 {
629 code: normalizeIndent`
630 // Valid because functions created with useEffectEvent can be called in a useEffect.
631 function MyComponent({ theme }) {
632 const onClick = useEffectEvent(() => {
633 showNotification(theme);
634 });
635 useEffect(() => {
636 onClick();
637 });
638 React.useEffect(() => {
639 onClick();
640 });
641 }
642 `,
643 },
644 {
645 syntax: 'flow',
646 code: normalizeIndent`
647 // Component syntax version
648 // Valid because functions created with useEffectEvent can be called in a useEffect.
649 component MyComponent(theme: any) {
650 const onClick = useEffectEvent(() => {
651 showNotification(theme);
652 });
653 useEffect(() => {
654 onClick();
655 });
656 React.useEffect(() => {
657 onClick();
658 });
659 }
660 `,
661 },
662 {
663 code: normalizeIndent`
664 // Valid because functions created with useEffectEvent can be passed by reference in useEffect
665 // and useEffectEvent.
666 function MyComponent({ theme }) {
667 const onClick = useEffectEvent(() => {
668 showNotification(theme);
669 });
670 const onClick2 = useEffectEvent(() => {
671 debounce(onClick);
672 debounce(() => onClick());
673 debounce(() => { onClick() });
674 deboucne(() => debounce(onClick));
675 });
676 useEffect(() => {
677 let id = setInterval(() => onClick(), 100);
678 return () => clearInterval(onClick);
679 }, []);
680 React.useEffect(() => {
681 let id = setInterval(() => onClick(), 100);
682 return () => clearInterval(onClick);
683 }, []);
684 return null;
685 }
686 `,
687 },
688 {
689 syntax: 'flow',
690 code: normalizeIndent`
691 // Component syntax version
692 // Valid because functions created with useEffectEvent can be passed by reference in useEffect
693 // and useEffectEvent.
694 component MyComponent(theme: any) {
695 const onClick = useEffectEvent(() => {
696 showNotification(theme);
697 });
698 const onClick2 = useEffectEvent(() => {
699 debounce(onClick);
700 debounce(() => onClick());
701 debounce(() => { onClick() });
702 deboucne(() => debounce(onClick));
703 });
704 useEffect(() => {
705 let id = setInterval(() => onClick(), 100);
706 return () => clearInterval(onClick);
707 }, []);
708 React.useEffect(() => {
709 let id = setInterval(() => onClick(), 100);
710 return () => clearInterval(onClick);
711 }, []);
712 return null;
713 }
714 `,
715 },
716 {
717 code: normalizeIndent`
718 function MyComponent({ theme }) {
719 useEffect(() => {
720 onClick();
721 });
722 const onClick = useEffectEvent(() => {
723 showNotification(theme);
724 });
725 }
726 `,
727 },
728 {
729 syntax: 'flow',
730 code: normalizeIndent`
731 // Component syntax version
732 component MyComponent(theme: any) {
733 useEffect(() => {
734 onClick();
735 });
736 const onClick = useEffectEvent(() => {
737 showNotification(theme);
738 });
739 }
740 `,
741 },
742 {
743 code: normalizeIndent`
744 function MyComponent({ theme }) {
745 // Can receive arguments
746 const onEvent = useEffectEvent((text) => {
747 console.log(text);
748 });
749
750 useEffect(() => {
751 onEvent('Hello world');
752 });
753 React.useEffect(() => {
754 onEvent('Hello world');
755 });
756 }
757 `,
758 },
759 {
760 syntax: 'flow',
761 code: normalizeIndent`
762 // Component syntax version
763 component MyComponent(theme: any) {
764 // Can receive arguments
765 const onEvent = useEffectEvent((text) => {
766 console.log(text);
767 });
768
769 useEffect(() => {
770 onEvent('Hello world');
771 });
772 React.useEffect(() => {
773 onEvent('Hello world');
774 });
775 }
776 `,
777 },
778 {
779 code: normalizeIndent`
780 // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
781 function MyComponent({ theme }) {
782 const onClick = useEffectEvent(() => {
783 showNotification(theme);
784 });
785 useLayoutEffect(() => {
786 onClick();
787 });
788 React.useLayoutEffect(() => {
789 onClick();
790 });
791 }
792 `,
793 },
794 {
795 syntax: 'flow',
796 code: normalizeIndent`
797 // Component syntax version
798 // Valid because functions created with useEffectEvent can be called in useLayoutEffect.
799 component MyComponent(theme: any) {
800 const onClick = useEffectEvent(() => {
801 showNotification(theme);
802 });
803 useLayoutEffect(() => {
804 onClick();
805 });
806 React.useLayoutEffect(() => {
807 onClick();
808 });
809 }
810 `,
811 },
812 {
813 code: normalizeIndent`
814 // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
815 function MyComponent({ theme }) {
816 const onClick = useEffectEvent(() => {
817 showNotification(theme);
818 });
819 useInsertionEffect(() => {
820 onClick();
821 });
822 React.useInsertionEffect(() => {
823 onClick();
824 });
825 }
826 `,
827 },
828 {
829 syntax: 'flow',
830 code: normalizeIndent`
831 // Component syntax version
832 // Valid because functions created with useEffectEvent can be called in useInsertionEffect.
833 component MyComponent(theme) {
834 const onClick = useEffectEvent(() => {
835 showNotification(theme);
836 });
837 useInsertionEffect(() => {
838 onClick();
839 });
840 React.useInsertionEffect(() => {
841 onClick();
842 });
843 }
844 `,
845 },
846 {
847 code: normalizeIndent`
848 // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect
849 // and useInsertionEffect.
850 function MyComponent({ theme }) {
851 const onClick = useEffectEvent(() => {
852 showNotification(theme);
853 });
854 const onClick2 = useEffectEvent(() => {
855 debounce(onClick);
856 debounce(() => onClick());
857 debounce(() => { onClick() });
858 deboucne(() => debounce(onClick));
859 });
860 useLayoutEffect(() => {
861 let id = setInterval(() => onClick(), 100);
862 return () => clearInterval(onClick);
863 }, []);
864 React.useLayoutEffect(() => {
865 let id = setInterval(() => onClick(), 100);
866 return () => clearInterval(onClick);
867 }, []);
868 useInsertionEffect(() => {
869 let id = setInterval(() => onClick(), 100);
870 return () => clearInterval(onClick);
871 }, []);
872 React.useInsertionEffect(() => {
873 let id = setInterval(() => onClick(), 100);
874 return () => clearInterval(onClick);
875 }, []);
876 return null;
877 }
878 `,
879 },
880 {
881 syntax: 'flow',
882 code: normalizeIndent`
883 // Component syntax version
884 // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect.
885 // and useInsertionEffect.
886 component MyComponent(theme: any) {
887 const onClick = useEffectEvent(() => {
888 showNotification(theme);
889 });
890 const onClick2 = useEffectEvent(() => {
891 debounce(onClick);
892 debounce(() => onClick());
893 debounce(() => { onClick() });
894 deboucne(() => debounce(onClick));
895 });
896 useLayoutEffect(() => {
897 let id = setInterval(() => onClick(), 100);
898 return () => clearInterval(onClick);
899 }, []);
900 React.useLayoutEffect(() => {
901 let id = setInterval(() => onClick(), 100);
902 return () => clearInterval(onClick);
903 }, []);
904 useInsertionEffect(() => {
905 let id = setInterval(() => onClick(), 100);
906 return () => clearInterval(onClick);
907 }, []);
908 React.useInsertionEffect(() => {
909 let id = setInterval(() => onClick(), 100);
910 return () => clearInterval(onClick);
911 }, []);
912 return null;
913 }
914 `,
915 },
916 ],
917 invalid: [
918 {
919 syntax: 'flow',
920 code: normalizeIndent`
921 component Button(cond: boolean) {
922 if (cond) {
923 useConditionalHook();
924 }
925 }
926 `,
927 errors: [conditionalError('useConditionalHook')],
928 },
929 {
930 syntax: 'flow',
931 code: normalizeIndent`
932 hook useTest(cond: boolean) {
933 if (cond) {
934 useConditionalHook();
935 }
936 }
937 `,
938 errors: [conditionalError('useConditionalHook')],
939 },
940 {
941 code: normalizeIndent`
942 // Invalid because it's dangerous and might not warn otherwise.
943 // This *must* be invalid.
944 function ComponentWithConditionalHook() {
945 if (cond) {
946 useConditionalHook();
947 }
948 }
949 `,
950 errors: [conditionalError('useConditionalHook')],
951 },
952 {
953 code: normalizeIndent`
954 Hook.useState();
955 Hook._useState();
956 Hook.use42();
957 Hook.useHook();
958 Hook.use_hook();
959 `,
960 errors: [
961 topLevelError('Hook.useState'),
962 topLevelError('Hook.use42'),
963 topLevelError('Hook.useHook'),
964 ],
965 },
966 {
967 code: normalizeIndent`
968 class C {
969 m() {
970 This.useHook();
971 Super.useHook();
972 }
973 }
974 `,
975 errors: [classError('This.useHook'), classError('Super.useHook')],
976 },
977 {
978 code: normalizeIndent`
979 // This is a false positive (it's valid) that unfortunately
980 // we cannot avoid. Prefer to rename it to not start with "use"
981 class Foo extends Component {
982 render() {
983 if (cond) {
984 FooStore.useFeatureFlag();
985 }
986 }
987 }
988 `,
989 errors: [classError('FooStore.useFeatureFlag')],
990 },
991 {
992 code: normalizeIndent`
993 // Invalid because it's dangerous and might not warn otherwise.
994 // This *must* be invalid.
995 function ComponentWithConditionalHook() {
996 if (cond) {
997 Namespace.useConditionalHook();
998 }
999 }
1000 `,
1001 errors: [conditionalError('Namespace.useConditionalHook')],
1002 },
1003 {
1004 code: normalizeIndent`
1005 // Invalid because it's dangerous and might not warn otherwise.
1006 // This *must* be invalid.
1007 function createComponent() {
1008 return function ComponentWithConditionalHook() {
1009 if (cond) {
1010 useConditionalHook();
1011 }
1012 }
1013 }
1014 `,
1015 errors: [conditionalError('useConditionalHook')],
1016 },
1017 {
1018 code: normalizeIndent`
1019 // Invalid because it's dangerous and might not warn otherwise.
1020 // This *must* be invalid.
1021 function useHookWithConditionalHook() {
1022 if (cond) {
1023 useConditionalHook();
1024 }
1025 }
1026 `,
1027 errors: [conditionalError('useConditionalHook')],
1028 },
1029 {
1030 code: normalizeIndent`
1031 // Invalid because it's dangerous and might not warn otherwise.
1032 // This *must* be invalid.
1033 function createHook() {
1034 return function useHookWithConditionalHook() {
1035 if (cond) {
1036 useConditionalHook();
1037 }
1038 }
1039 }
1040 `,
1041 errors: [conditionalError('useConditionalHook')],
1042 },
1043 {
1044 code: normalizeIndent`
1045 // Invalid because it's dangerous and might not warn otherwise.
1046 // This *must* be invalid.
1047 function ComponentWithTernaryHook() {
1048 cond ? useTernaryHook() : null;
1049 }
1050 `,
1051 errors: [conditionalError('useTernaryHook')],
1052 },
1053 {
1054 code: normalizeIndent`
1055 // Invalid because it's a common misunderstanding.
1056 // We *could* make it valid but the runtime error could be confusing.
1057 function ComponentWithHookInsideCallback() {
1058 useEffect(() => {
1059 useHookInsideCallback();
1060 });
1061 }
1062 `,
1063 errors: [genericError('useHookInsideCallback')],
1064 },
1065 {
1066 code: normalizeIndent`
1067 // Invalid because it's a common misunderstanding.
1068 // We *could* make it valid but the runtime error could be confusing.
1069 function createComponent() {
1070 return function ComponentWithHookInsideCallback() {
1071 useEffect(() => {
1072 useHookInsideCallback();
1073 });
1074 }
1075 }
1076 `,
1077 errors: [genericError('useHookInsideCallback')],
1078 },
1079 {
1080 code: normalizeIndent`
1081 // Invalid because it's a common misunderstanding.
1082 // We *could* make it valid but the runtime error could be confusing.
1083 const ComponentWithHookInsideCallback = React.forwardRef((props, ref) => {
1084 useEffect(() => {
1085 useHookInsideCallback();
1086 });
1087 return <button {...props} ref={ref} />
1088 });
1089 `,
1090 errors: [genericError('useHookInsideCallback')],
1091 },
1092 {
1093 code: normalizeIndent`
1094 // Invalid because it's a common misunderstanding.
1095 // We *could* make it valid but the runtime error could be confusing.
1096 const ComponentWithHookInsideCallback = React.memo(props => {
1097 useEffect(() => {
1098 useHookInsideCallback();
1099 });
1100 return <button {...props} />
1101 });
1102 `,
1103 errors: [genericError('useHookInsideCallback')],
1104 },
1105 {
1106 code: normalizeIndent`
1107 // Invalid because it's a common misunderstanding.
1108 // We *could* make it valid but the runtime error could be confusing.
1109 function ComponentWithHookInsideCallback() {
1110 function handleClick() {
1111 useState();
1112 }
1113 }
1114 `,
1115 errors: [functionError('useState', 'handleClick')],
1116 },
1117 {
1118 code: normalizeIndent`
1119 // Invalid because it's a common misunderstanding.
1120 // We *could* make it valid but the runtime error could be confusing.
1121 function createComponent() {
1122 return function ComponentWithHookInsideCallback() {
1123 function handleClick() {
1124 useState();
1125 }
1126 }
1127 }
1128 `,
1129 errors: [functionError('useState', 'handleClick')],
1130 },
1131 {
1132 code: normalizeIndent`
1133 // Invalid because it's dangerous and might not warn otherwise.
1134 // This *must* be invalid.
1135 function ComponentWithHookInsideLoop() {
1136 while (cond) {
1137 useHookInsideLoop();
1138 }
1139 }
1140 `,
1141 errors: [loopError('useHookInsideLoop')],
1142 },
1143 {
1144 code: normalizeIndent`
1145 // Invalid because it's dangerous and might not warn otherwise.
1146 // This *must* be invalid.
1147 function ComponentWithHookInsideLoop() {
1148 do {
1149 useHookInsideLoop();
1150 } while (cond);
1151 }
1152 `,
1153 errors: [loopError('useHookInsideLoop')],
1154 },
1155 {
1156 code: normalizeIndent`
1157 // Invalid because it's dangerous and might not warn otherwise.
1158 // This *must* be invalid.
1159 function ComponentWithHookInsideLoop() {
1160 do {
1161 foo();
1162 } while (useHookInsideLoop());
1163 }
1164 `,
1165 errors: [loopError('useHookInsideLoop')],
1166 },
1167 {
1168 code: normalizeIndent`
1169 // Invalid because it's dangerous and might not warn otherwise.
1170 // This *must* be invalid.
1171 function renderItem() {
1172 useState();
1173 }
1174
1175 function List(props) {
1176 return props.items.map(renderItem);
1177 }
1178 `,
1179 errors: [functionError('useState', 'renderItem')],
1180 },
1181 {
1182 code: normalizeIndent`
1183 // Currently invalid because it violates the convention and removes the "taint"
1184 // from a hook. We *could* make it valid to avoid some false positives but let's
1185 // ensure that we don't break the "renderItem" and "normalFunctionWithConditionalHook"
1186 // cases which must remain invalid.
1187 function normalFunctionWithHook() {
1188 useHookInsideNormalFunction();
1189 }
1190 `,
1191 errors: [
1192 functionError('useHookInsideNormalFunction', 'normalFunctionWithHook'),
1193 ],
1194 },
1195 {
1196 code: normalizeIndent`
1197 // These are neither functions nor hooks.
1198 function _normalFunctionWithHook() {
1199 useHookInsideNormalFunction();
1200 }
1201 function _useNotAHook() {
1202 useHookInsideNormalFunction();
1203 }
1204 `,
1205 errors: [
1206 functionError('useHookInsideNormalFunction', '_normalFunctionWithHook'),
1207 functionError('useHookInsideNormalFunction', '_useNotAHook'),
1208 ],
1209 },
1210 {
1211 code: normalizeIndent`
1212 // Invalid because it's dangerous and might not warn otherwise.
1213 // This *must* be invalid.
1214 function normalFunctionWithConditionalHook() {
1215 if (cond) {
1216 useHookInsideNormalFunction();
1217 }
1218 }
1219 `,
1220 errors: [
1221 functionError(
1222 'useHookInsideNormalFunction',
1223 'normalFunctionWithConditionalHook'
1224 ),
1225 ],
1226 },
1227 {
1228 code: normalizeIndent`
1229 // Invalid because it's dangerous and might not warn otherwise.
1230 // This *must* be invalid.
1231 function useHookInLoops() {
1232 while (a) {
1233 useHook1();
1234 if (b) return;
1235 useHook2();
1236 }
1237 while (c) {
1238 useHook3();
1239 if (d) return;
1240 useHook4();
1241 }
1242 }
1243 `,
1244 errors: [
1245 loopError('useHook1'),
1246 loopError('useHook2'),
1247 loopError('useHook3'),
1248 loopError('useHook4'),
1249 ],
1250 },
1251 {
1252 code: normalizeIndent`
1253 // Invalid because it's dangerous and might not warn otherwise.
1254 // This *must* be invalid.
1255 function useHookInLoops() {
1256 while (a) {
1257 useHook1();
1258 if (b) continue;
1259 useHook2();
1260 }
1261 }
1262 `,
1263 errors: [loopError('useHook1'), loopError('useHook2', true)],
1264 },
1265 {
1266 code: normalizeIndent`
1267 // Invalid because it's dangerous and might not warn otherwise.
1268 // This *must* be invalid.
1269 function useHookInLoops() {
1270 do {
1271 useHook1();
1272 if (a) return;
1273 useHook2();
1274 } while (b);
1275
1276 do {
1277 useHook3();
1278 if (c) return;
1279 useHook4();
1280 } while (d)
1281 }
1282 `,
1283 errors: [
1284 loopError('useHook1'),
1285 loopError('useHook2'),
1286 loopError('useHook3'),
1287 loopError('useHook4'),
1288 ],
1289 },
1290 {
1291 code: normalizeIndent`
1292 // Invalid because it's dangerous and might not warn otherwise.
1293 // This *must* be invalid.
1294 function useHookInLoops() {
1295 do {
1296 useHook1();
1297 if (a) continue;
1298 useHook2();
1299 } while (b);
1300 }
1301 `,
1302 errors: [loopError('useHook1'), loopError('useHook2', true)],
1303 },
1304 {
1305 code: normalizeIndent`
1306 // Invalid because it's dangerous and might not warn otherwise.
1307 // This *must* be invalid.
1308 function useLabeledBlock() {
1309 label: {
1310 if (a) break label;
1311 useHook();
1312 }
1313 }
1314 `,
1315 errors: [conditionalError('useHook')],
1316 },
1317 {
1318 code: normalizeIndent`
1319 // Currently invalid.
1320 // These are variations capturing the current heuristic--
1321 // we only allow hooks in PascalCase or useFoo functions.
1322 // We *could* make some of these valid. But before doing it,
1323 // consider specific cases documented above that contain reasoning.
1324 function a() { useState(); }
1325 const whatever = function b() { useState(); };
1326 const c = () => { useState(); };
1327 let d = () => useState();
1328 e = () => { useState(); };
1329 ({f: () => { useState(); }});
1330 ({g() { useState(); }});
1331 const {j = () => { useState(); }} = {};
1332 ({k = () => { useState(); }} = {});
1333 `,
1334 errors: [
1335 functionError('useState', 'a'),
1336 functionError('useState', 'b'),
1337 functionError('useState', 'c'),
1338 functionError('useState', 'd'),
1339 functionError('useState', 'e'),
1340 functionError('useState', 'f'),
1341 functionError('useState', 'g'),
1342 functionError('useState', 'j'),
1343 functionError('useState', 'k'),
1344 ],
1345 },
1346 {
1347 code: normalizeIndent`
1348 // Invalid because it's dangerous and might not warn otherwise.
1349 // This *must* be invalid.
1350 function useHook() {
1351 if (a) return;
1352 useState();
1353 }
1354 `,
1355 errors: [conditionalError('useState', true)],
1356 },
1357 {
1358 code: normalizeIndent`
1359 // Invalid because it's dangerous and might not warn otherwise.
1360 // This *must* be invalid.
1361 function useHook() {
1362 if (a) return;
1363 if (b) {
1364 console.log('true');
1365 } else {
1366 console.log('false');
1367 }
1368 useState();
1369 }
1370 `,
1371 errors: [conditionalError('useState', true)],
1372 },
1373 {
1374 code: normalizeIndent`
1375 // Invalid because it's dangerous and might not warn otherwise.
1376 // This *must* be invalid.
1377 function useHook() {
1378 if (b) {
1379 console.log('true');
1380 } else {
1381 console.log('false');
1382 }
1383 if (a) return;
1384 useState();
1385 }
1386 `,
1387 errors: [conditionalError('useState', true)],
1388 },
1389 {
1390 code: normalizeIndent`
1391 // Invalid because it's dangerous and might not warn otherwise.
1392 // This *must* be invalid.
1393 function useHook() {
1394 a && useHook1();
1395 b && useHook2();
1396 }
1397 `,
1398 errors: [conditionalError('useHook1'), conditionalError('useHook2')],
1399 },
1400 {
1401 code: normalizeIndent`
1402 // Invalid because it's dangerous and might not warn otherwise.
1403 // This *must* be invalid.
1404 function useHook() {
1405 try {
1406 f();
1407 useState();
1408 } catch {}
1409 }
1410 `,
1411 errors: [
1412 // NOTE: This is an error since `f()` could possibly throw.
1413 conditionalError('useState'),
1414 ],
1415 },
1416 {
1417 code: normalizeIndent`
1418 // Invalid because it's dangerous and might not warn otherwise.
1419 // This *must* be invalid.
1420 function useHook({ bar }) {
1421 let foo1 = bar && useState();
1422 let foo2 = bar || useState();
1423 let foo3 = bar ?? useState();
1424 }
1425 `,
1426 errors: [
1427 conditionalError('useState'),
1428 conditionalError('useState'),
1429 conditionalError('useState'),
1430 ],
1431 },
1432 {
1433 code: normalizeIndent`
1434 // Invalid because it's dangerous and might not warn otherwise.
1435 // This *must* be invalid.
1436 const FancyButton = React.forwardRef((props, ref) => {
1437 if (props.fancy) {
1438 useCustomHook();
1439 }
1440 return <button ref={ref}>{props.children}</button>;
1441 });
1442 `,
1443 errors: [conditionalError('useCustomHook')],
1444 },
1445 {
1446 code: normalizeIndent`
1447 // Invalid because it's dangerous and might not warn otherwise.
1448 // This *must* be invalid.
1449 const FancyButton = forwardRef(function(props, ref) {
1450 if (props.fancy) {
1451 useCustomHook();
1452 }
1453 return <button ref={ref}>{props.children}</button>;
1454 });
1455 `,
1456 errors: [conditionalError('useCustomHook')],
1457 },
1458 {
1459 code: normalizeIndent`
1460 // Invalid because it's dangerous and might not warn otherwise.
1461 // This *must* be invalid.
1462 const MemoizedButton = memo(function(props) {
1463 if (props.fancy) {
1464 useCustomHook();
1465 }
1466 return <button>{props.children}</button>;
1467 });
1468 `,
1469 errors: [conditionalError('useCustomHook')],
1470 },
1471 {
1472 code: normalizeIndent`
1473 // This is invalid because "use"-prefixed functions used in named
1474 // functions are assumed to be hooks.
1475 React.unknownFunction(function notAComponent(foo, bar) {
1476 useProbablyAHook(bar)
1477 });
1478 `,
1479 errors: [functionError('useProbablyAHook', 'notAComponent')],
1480 },
1481 {
1482 code: normalizeIndent`
1483 // Invalid because it's dangerous.
1484 // Normally, this would crash, but not if you use inline requires.
1485 // This *must* be invalid.
1486 // It's expected to have some false positives, but arguably
1487 // they are confusing anyway due to the use*() convention
1488 // already being associated with Hooks.
1489 useState();
1490 if (foo) {
1491 const foo = React.useCallback(() => {});
1492 }
1493 useCustomHook();
1494 `,
1495 errors: [
1496 topLevelError('useState'),
1497 topLevelError('React.useCallback'),
1498 topLevelError('useCustomHook'),
1499 ],
1500 },
1501 {
1502 code: normalizeIndent`
1503 // Technically this is a false positive.
1504 // We *could* make it valid (and it used to be).
1505 //
1506 // However, top-level Hook-like calls can be very dangerous
1507 // in environments with inline requires because they can mask
1508 // the runtime error by accident.
1509 // So we prefer to disallow it despite the false positive.
1510
1511 const {createHistory, useBasename} = require('history-2.1.2');
1512 const browserHistory = useBasename(createHistory)({
1513 basename: '/',
1514 });
1515 `,
1516 errors: [topLevelError('useBasename')],
1517 },
1518 {
1519 code: normalizeIndent`
1520 class ClassComponentWithFeatureFlag extends React.Component {
1521 render() {
1522 if (foo) {
1523 useFeatureFlag();
1524 }
1525 }
1526 }
1527 `,
1528 errors: [classError('useFeatureFlag')],
1529 },
1530 {
1531 code: normalizeIndent`
1532 class ClassComponentWithHook extends React.Component {
1533 render() {
1534 React.useState();
1535 }
1536 }
1537 `,
1538 errors: [classError('React.useState')],
1539 },
1540 {
1541 code: normalizeIndent`
1542 (class {useHook = () => { useState(); }});
1543 `,
1544 errors: [classError('useState')],
1545 },
1546 {
1547 code: normalizeIndent`
1548 (class {useHook() { useState(); }});
1549 `,
1550 errors: [classError('useState')],
1551 },
1552 {
1553 code: normalizeIndent`
1554 (class {h = () => { useState(); }});
1555 `,
1556 errors: [classError('useState')],
1557 },
1558 {
1559 code: normalizeIndent`
1560 (class {i() { useState(); }});
1561 `,
1562 errors: [classError('useState')],
1563 },
1564 {
1565 code: normalizeIndent`
1566 async function AsyncComponent() {
1567 useState();
1568 }
1569 `,
1570 errors: [asyncComponentHookError('useState')],
1571 },
1572 {
1573 code: normalizeIndent`
1574 async function useAsyncHook() {
1575 useState();
1576 }
1577 `,
1578 errors: [asyncComponentHookError('useState')],
1579 },
1580 {
1581 code: normalizeIndent`
1582 async function Page() {
1583 useId();
1584 React.useId();
1585 }
1586 `,
1587 errors: [
1588 asyncComponentHookError('useId'),
1589 asyncComponentHookError('React.useId'),
1590 ],
1591 },
1592 {
1593 code: normalizeIndent`
1594 async function useAsyncHook() {
1595 useId();
1596 }
1597 `,
1598 errors: [asyncComponentHookError('useId')],
1599 },
1600 {
1601 code: normalizeIndent`
1602 async function notAHook() {
1603 useId();
1604 }
1605 `,
1606 errors: [functionError('useId', 'notAHook')],
1607 },
1608 {
1609 code: normalizeIndent`
1610 Hook.use();
1611 Hook._use();
1612 Hook.useState();
1613 Hook._useState();
1614 Hook.use42();
1615 Hook.useHook();
1616 Hook.use_hook();
1617 `,
1618 errors: [
1619 topLevelError('Hook.use'),
1620 topLevelError('Hook.useState'),
1621 topLevelError('Hook.use42'),
1622 topLevelError('Hook.useHook'),
1623 ],
1624 },
1625 {
1626 code: normalizeIndent`
1627 function notAComponent() {
1628 use(promise);
1629 }
1630 `,
1631 errors: [functionError('use', 'notAComponent')],
1632 },
1633 {
1634 code: normalizeIndent`
1635 const text = use(promise);
1636 function App() {
1637 return <Text text={text} />
1638 }
1639 `,
1640 errors: [topLevelError('use')],
1641 },
1642 {
1643 code: normalizeIndent`
1644 class C {
1645 m() {
1646 use(promise);
1647 }
1648 }
1649 `,
1650 errors: [classError('use')],
1651 },
1652 {
1653 code: normalizeIndent`
1654 async function AsyncComponent() {
1655 use();
1656 }
1657 `,
1658 errors: [asyncComponentHookError('use')],
1659 },
1660 {
1661 code: normalizeIndent`
1662 function App({p1, p2}) {
1663 try {
1664 use(p1);
1665 } catch (error) {
1666 console.error(error);
1667 }
1668 use(p2);
1669 return <div>App</div>;
1670 }
1671 `,
1672 errors: [tryCatchUseError('use')],
1673 },
1674 {
1675 code: normalizeIndent`
1676 function App({p1, p2}) {
1677 try {
1678 doSomething();
1679 } catch {
1680 use(p1);
1681 }
1682 use(p2);
1683 return <div>App</div>;
1684 }
1685 `,
1686 errors: [tryCatchUseError('use')],
1687 },
1688 {
1689 code: normalizeIndent`
1690 // Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
1691 function MyComponent({ theme }) {
1692 const onClick = useEffectEvent(() => {
1693 showNotification(theme);
1694 });
1695 useCustomHook(() => {
1696 onClick();
1697 });
1698 }
1699 `,
1700 errors: [useEffectEventError('onClick', true)],
1701 },
1702 {
1703 syntax: 'flow',
1704 code: normalizeIndent`
1705 // Component syntax version
1706 // Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
1707 component MyComponent() {
1708 const onClick = useEffectEvent(() => {
1709 showNotification(theme);
1710 });
1711 useCustomHook(() => {
1712 onClick();
1713 });
1714 }
1715 `,
1716 errors: [useEffectEventError('onClick', true)],
1717 },
1718 {
1719 code: normalizeIndent`
1720 // Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
1721 function MyComponent({ theme }) {
1722 const onClick = useEffectEvent(() => {
1723 showNotification(theme);
1724 });
1725 useWrongHook(() => {
1726 onClick();
1727 });
1728 }
1729 `,
1730 settings: {
1731 'react-hooks': {
1732 additionalEffectHooks: 'useMyEffect',
1733 },
1734 },
1735 errors: [useEffectEventError('onClick', true)],
1736 },
1737 {
1738 syntax: 'flow',
1739 code: normalizeIndent`
1740 // Component syntax version
1741 // Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
1742 component MyComponent(theme: any) {
1743 const onClick = useEffectEvent(() => {
1744 showNotification(theme);
1745 });
1746 useWrongHook(() => {
1747 onClick();
1748 });
1749 }
1750 `,
1751 settings: {
1752 'react-hooks': {
1753 additionalEffectHooks: 'useMyEffect',
1754 },
1755 },
1756 errors: [useEffectEventError('onClick', true)],
1757 },
1758 {
1759 code: normalizeIndent`
1760 function MyComponent({ theme }) {
1761 const onClick = useEffectEvent(() => {
1762 showNotification(theme);
1763 });
1764 return <Child onClick={onClick}></Child>;
1765 }
1766 `,
1767 errors: [useEffectEventError('onClick', false)],
1768 },
1769 {
1770 syntax: 'flow',
1771 code: normalizeIndent`
1772 // Component syntax version
1773 component MyComponent(theme: any) {
1774 const onClick = useEffectEvent(() => {
1775 showNotification(theme);
1776 });
1777 return <Child onClick={onClick}></Child>;
1778 }
1779 `,
1780 errors: [useEffectEventError('onClick', false)],
1781 },
1782 {
1783 code: normalizeIndent`
1784 // Invalid because useEffectEvent is being passed down
1785 function MyComponent({ theme }) {
1786 return <Child onClick={useEffectEvent(() => {
1787 showNotification(theme);
1788 })} />;
1789 }
1790 `,
1791 errors: [{...useEffectEventError(null, false), line: 4}],
1792 },
1793 {
1794 syntax: 'flow',
1795 code: normalizeIndent`
1796 // Component syntax version
1797 // Invalid because useEffectEvent is being passed down
1798 component MyComponent(theme: any) {
1799 return <Child onClick={useEffectEvent(() => {
1800 showNotification(theme);
1801 })} />;
1802 }
1803 `,
1804 errors: [{...useEffectEventError(null, false), line: 5}],
1805 },
1806 {
1807 code: normalizeIndent`
1808 // This should error even though it shares an identifier name with the below
1809 function MyComponent({theme}) {
1810 const onClick = useEffectEvent(() => {
1811 showNotification(theme)
1812 });
1813 return <Child onClick={onClick} />
1814 }
1815
1816 // The useEffectEvent function shares an identifier name with the above
1817 function MyOtherComponent({theme}) {
1818 const onClick = useEffectEvent(() => {
1819 showNotification(theme)
1820 });
1821 return <Child onClick={() => onClick()} />
1822 }
1823
1824 // The useEffectEvent function shares an identifier name with the above
1825 function MyLastComponent({theme}) {
1826 const onClick = useEffectEvent(() => {
1827 showNotification(theme)
1828 });
1829 useEffect(() => {
1830 onClick(); // No error here, errors on all other uses
1831 onClick;
1832 })
1833 return <Child />
1834 }
1835 `,
1836 errors: [
1837 {...useEffectEventError('onClick', false), line: 7},
1838 {...useEffectEventError('onClick', true), line: 15},
1839 ],
1840 },
1841 {
1842 syntax: 'flow',
1843 code: normalizeIndent`
1844 // Component syntax version
1845 // This should error even though it shares an identifier name with the below
1846 component MyComponent(theme: any) {
1847 const onClick = useEffectEvent(() => {
1848 showNotification(theme)
1849 });
1850 return <Child onClick={onClick} />
1851 }
1852
1853 // The useEffectEvent function shares an identifier name with the above
1854 component MyOtherComponent(theme: any) {
1855 const onClick = useEffectEvent(() => {
1856 showNotification(theme)
1857 });
1858 return <Child onClick={() => onClick()} />
1859 }
1860
1861 // The useEffectEvent function shares an identifier name with the above
1862 component MyLastComponent(theme: any) {
1863 const onClick = useEffectEvent(() => {
1864 showNotification(theme)
1865 });
1866 useEffect(() => {
1867 onClick(); // No error here, errors on all other uses
1868 onClick;
1869 })
1870 return <Child />
1871 }
1872 `,
1873 errors: [
1874 {...useEffectEventError('onClick', false), line: 8},
1875 {...useEffectEventError('onClick', true), line: 16},
1876 ],
1877 },
1878 {
1879 code: normalizeIndent`
1880 const MyComponent = ({ theme }) => {
1881 const onClick = useEffectEvent(() => {
1882 showNotification(theme);
1883 });
1884 return <Child onClick={onClick}></Child>;
1885 }
1886 `,
1887 errors: [useEffectEventError('onClick', false)],
1888 },
1889 {
1890 code: normalizeIndent`
1891 // Invalid because onClick is being aliased to foo but not invoked
1892 function MyComponent({ theme }) {
1893 const onClick = useEffectEvent(() => {
1894 showNotification(theme);
1895 });
1896 let foo = onClick;
1897 return <Bar onClick={foo} />
1898 }
1899 `,
1900 errors: [{...useEffectEventError('onClick', false), line: 7}],
1901 },
1902 {
1903 syntax: 'flow',
1904 code: normalizeIndent`
1905 // Component syntax version
1906 // Invalid because onClick is being aliased to foo but not invoked
1907 component MyComponent(theme: any) {
1908 const onClick = useEffectEvent(() => {
1909 showNotification(theme);
1910 });
1911 let foo = onClick;
1912 return <Bar onClick={foo} />
1913 }
1914 `,
1915 errors: [{...useEffectEventError('onClick', false), line: 8}],
1916 },
1917 {
1918 code: normalizeIndent`
1919 // Should error because it's being passed down to JSX, although it's been referenced once
1920 // in an effect
1921 function MyComponent({ theme }) {
1922 const onClick = useEffectEvent(() => {
1923 showNotification(them);
1924 });
1925 useEffect(() => {
1926 setTimeout(onClick, 100);
1927 });
1928 return <Child onClick={onClick} />
1929 }
1930 `,
1931 errors: [useEffectEventError('onClick', false)],
1932 },
1933 {
1934 syntax: 'flow',
1935 code: normalizeIndent`
1936 // Component syntax version
1937 // Should error because it's being passed down to JSX, although it's been referenced once
1938 // in an effect
1939 component MyComponent(theme: any) {
1940 const onClick = useEffectEvent(() => {
1941 showNotification(them);
1942 });
1943 useEffect(() => {
1944 setTimeout(onClick, 100);
1945 });
1946 return <Child onClick={onClick} />
1947 }
1948 `,
1949 errors: [useEffectEventError('onClick', false)],
1950 },
1951 {
1952 code: normalizeIndent`
1953 // Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
1954 function MyComponent({ theme }) {
1955 const onClick = useEffectEvent(() => {
1956 showNotification(theme);
1957 });
1958 // error message 1
1959 const onClick2 = () => { onClick() };
1960 // error message 2
1961 const onClick3 = useCallback(() => onClick(), []);
1962 // error message 3
1963 const onClick4 = onClick;
1964 return <>
1965 {/** error message 4 */}
1966 <Child onClick={onClick}></Child>
1967 <Child onClick={onClick2}></Child>
1968 <Child onClick={onClick3}></Child>
1969 </>;
1970 }
1971 `,
1972 // Explicitly test error messages here for various cases
1973 errors: [
1974 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
1975 'Effects and Effect Events in the same component.',
1976 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
1977 'Effects and Effect Events in the same component.',
1978 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
1979 `Effects and Effect Events in the same component. ` +
1980 `It cannot be assigned to a variable or passed down.`,
1981 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
1982 `Effects and Effect Events in the same component. ` +
1983 `It cannot be assigned to a variable or passed down.`,
1984 ],
1985 },
1986 {
1987 syntax: 'flow',
1988 code: normalizeIndent`
1989 // Hook syntax version
1990 // Invalid because functions created with useEffectEvent cannot be called in arbitrary closures.
1991 hook useMyHook(theme: any) {
1992 const onClick = useEffectEvent(() => {
1993 showNotification(theme);
1994 });
1995 // error message 1
1996 const onClick2 = () => { onClick() };
1997 // error message 2
1998 const onClick3 = useCallback(() => onClick(), []);
1999 // error message 3
2000 const onClick4 = onClick;
2001 return <>
2002 {/** error message 4 */}
2003 <Child onClick={onClick}></Child>
2004 <Child onClick={onClick2}></Child>
2005 <Child onClick={onClick3}></Child>
2006 </>;
2007 }
2008 `,
2009 // Explicitly test error messages here for various cases
2010 errors: [
2011 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2012 'Effects and Effect Events in the same component.',
2013 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2014 'Effects and Effect Events in the same component.',
2015 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2016 `Effects and Effect Events in the same component. ` +
2017 `It cannot be assigned to a variable or passed down.`,
2018 `\`onClick\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2019 `Effects and Effect Events in the same component. ` +
2020 `It cannot be assigned to a variable or passed down.`,
2021 ],
2022 },
2023 ],
2024 };
2025
2026 function conditionalError(hook, hasPreviousFinalizer = false) {
2027 return {
2028 message:
2029 `React Hook "${hook}" is called conditionally. React Hooks must be ` +
2030 'called in the exact same order in every component render.' +
2031 (hasPreviousFinalizer
2032 ? ' Did you accidentally call a React Hook after an early return?'
2033 : ''),
2034 };
2035 }
2036
2037 function loopError(hook) {
2038 return {
2039 message:
2040 `React Hook "${hook}" may be executed more than once. Possibly ` +
2041 'because it is called in a loop. React Hooks must be called in the ' +
2042 'exact same order in every component render.',
2043 };
2044 }
2045
2046 function functionError(hook, fn) {
2047 return {
2048 message:
2049 `React Hook "${hook}" is called in function "${fn}" that is neither ` +
2050 'a React function component nor a custom React Hook function.' +
2051 ' React component names must start with an uppercase letter.' +
2052 ' React Hook names must start with the word "use".',
2053 };
2054 }
2055
2056 function genericError(hook) {
2057 return {
2058 message:
2059 `React Hook "${hook}" cannot be called inside a callback. React Hooks ` +
2060 'must be called in a React function component or a custom React ' +
2061 'Hook function.',
2062 };
2063 }
2064
2065 function topLevelError(hook) {
2066 return {
2067 message:
2068 `React Hook "${hook}" cannot be called at the top level. React Hooks ` +
2069 'must be called in a React function component or a custom React ' +
2070 'Hook function.',
2071 };
2072 }
2073
2074 function classError(hook) {
2075 return {
2076 message:
2077 `React Hook "${hook}" cannot be called in a class component. React Hooks ` +
2078 'must be called in a React function component or a custom React ' +
2079 'Hook function.',
2080 };
2081 }
2082
2083 function useEffectEventError(fn, called) {
2084 if (fn === null) {
2085 return {
2086 message:
2087 `React Hook "useEffectEvent" can only be called at the top level of your component.` +
2088 ` It cannot be passed down.`,
2089 };
2090 }
2091
2092 return {
2093 message:
2094 `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +
2095 'Effects and Effect Events in the same component.' +
2096 (called ? '' : ' It cannot be assigned to a variable or passed down.'),
2097 };
2098 }
2099
2100 function asyncComponentHookError(fn) {
2101 return {
2102 message: `React Hook "${fn}" cannot be called in an async function.`,
2103 };
2104 }
2105
2106 function tryCatchUseError(fn) {
2107 return {
2108 message: `React Hook "${fn}" cannot be called in a try/catch block.`,
2109 };
2110 }
2111
2112 // For easier local testing
2113 if (!process.env.CI) {
2114 let only = [];
2115 let skipped = [];
2116 [...allTests.valid, ...allTests.invalid].forEach(t => {
2117 if (t.skip) {
2118 delete t.skip;
2119 skipped.push(t);
2120 }
2121 if (t.only) {
2122 delete t.only;
2123 only.push(t);
2124 }
2125 });
2126 const predicate = t => {
2127 if (only.length > 0) {
2128 return only.indexOf(t) !== -1;
2129 }
2130 if (skipped.length > 0) {
2131 return skipped.indexOf(t) === -1;
2132 }
2133 return true;
2134 };
2135 allTests.valid = allTests.valid.filter(predicate);
2136 allTests.invalid = allTests.invalid.filter(predicate);
2137 }
2138
2139 function filteredTests(predicate) {
2140 return {
2141 valid: allTests.valid.filter(predicate),
2142 invalid: allTests.invalid.filter(predicate),
2143 };
2144 }
2145
2146 const flowTests = filteredTests(t => t.syntax == null || t.syntax === 'flow');
2147 const tests = filteredTests(t => t.syntax !== 'flow');
2148
2149 allTests.valid.forEach(t => delete t.syntax);
2150 allTests.invalid.forEach(t => delete t.syntax);
2151
2152 describe('rules-of-hooks/rules-of-hooks', () => {
2153 const parserOptionsV7 = {
2154 ecmaFeatures: {
2155 jsx: true,
2156 },
2157 ecmaVersion: 6,
2158 sourceType: 'module',
2159 };
2160
2161 const languageOptionsV9 = {
2162 ecmaVersion: 6,
2163 sourceType: 'module',
2164 parserOptions: {
2165 ecmaFeatures: {
2166 jsx: true,
2167 },
2168 },
2169 };
2170
2171 new ESLintTesterV7({
2172 parser: require.resolve('babel-eslint'),
2173 parserOptions: parserOptionsV7,
2174 }).run('eslint: v7, parser: babel-eslint', ReactHooksESLintRule, tests);
2175
2176 new ESLintTesterV9({
2177 languageOptions: {
2178 ...languageOptionsV9,
2179 parser: require('@babel/eslint-parser'),
2180 },
2181 }).run(
2182 'eslint: v9, parser: @babel/eslint-parser',
2183 ReactHooksESLintRule,
2184 tests
2185 );
2186
2187 new ESLintTesterV7({
2188 parser: require.resolve('hermes-eslint'),
2189 parserOptions: {
2190 sourceType: 'module',
2191 enableExperimentalComponentSyntax: true,
2192 },
2193 }).run('eslint: v7, parser: hermes-eslint', ReactHooksESLintRule, flowTests);
2194
2195 new ESLintTesterV9({
2196 languageOptions: {
2197 ...languageOptionsV9,
2198 parser: require('hermes-eslint'),
2199 parserOptions: {
2200 sourceType: 'module',
2201 enableExperimentalComponentSyntax: true,
2202 },
2203 },
2204 }).run('eslint: v9, parser: hermes-eslint', ReactHooksESLintRule, flowTests);
2205
2206 new ESLintTesterV7({
2207 parser: require.resolve('@typescript-eslint/parser-v2'),
2208 parserOptions: parserOptionsV7,
2209 }).run(
2210 'eslint: v7, parser: @typescript-eslint/parser@2.x',
2211 ReactHooksESLintRule,
2212 tests
2213 );
2214
2215 new ESLintTesterV9({
2216 languageOptions: {
2217 ...languageOptionsV9,
2218 parser: require('@typescript-eslint/parser-v2'),
2219 },
2220 }).run(
2221 'eslint: v9, parser: @typescript-eslint/parser@2.x',
2222 ReactHooksESLintRule,
2223 tests
2224 );
2225
2226 new ESLintTesterV7({
2227 parser: require.resolve('@typescript-eslint/parser-v3'),
2228 parserOptions: parserOptionsV7,
2229 }).run(
2230 'eslint: v7, parser: @typescript-eslint/parser@3.x',
2231 ReactHooksESLintRule,
2232 tests
2233 );
2234
2235 new ESLintTesterV9({
2236 languageOptions: {
2237 ...languageOptionsV9,
2238 parser: require('@typescript-eslint/parser-v3'),
2239 },
2240 }).run(
2241 'eslint: v9, parser: @typescript-eslint/parser@3.x',
2242 ReactHooksESLintRule,
2243 tests
2244 );
2245
2246 new ESLintTesterV7({
2247 parser: require.resolve('@typescript-eslint/parser-v4'),
2248 parserOptions: parserOptionsV7,
2249 }).run(
2250 'eslint: v7, parser: @typescript-eslint/parser@4.x',
2251 ReactHooksESLintRule,
2252 tests
2253 );
2254
2255 new ESLintTesterV9({
2256 languageOptions: {
2257 ...languageOptionsV9,
2258 parser: require('@typescript-eslint/parser-v4'),
2259 },
2260 }).run(
2261 'eslint: v9, parser: @typescript-eslint/parser@4.x',
2262 ReactHooksESLintRule,
2263 tests
2264 );
2265
2266 new ESLintTesterV7({
2267 parser: require.resolve('@typescript-eslint/parser-v5'),
2268 parserOptions: parserOptionsV7,
2269 }).run(
2270 'eslint: v7, parser: @typescript-eslint/parser@^5.0.0-0',
2271 ReactHooksESLintRule,
2272 tests
2273 );
2274
2275 new ESLintTesterV9({
2276 languageOptions: {
2277 ...languageOptionsV9,
2278 parser: require('@typescript-eslint/parser-v5'),
2279 },
2280 }).run(
2281 'eslint: v9, parser: @typescript-eslint/parser@^5.0.0',
2282 ReactHooksESLintRule,
2283 tests
2284 );
2285 });