1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @jest-environment node8 */910'use strict';1112const ESLintTesterV7 = require('eslint-v7').RuleTester;13const ESLintTesterV9 = require('eslint-v9').RuleTester;14const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks');15const ReactHooksESLintRule =16 ReactHooksESLintPlugin.default.rules['exhaustive-deps'];1718/**19 * A string template tag that removes padding from the left side of multi-line strings20 * @param {Array} strings array of code strings (only one expected)21 */22function 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}2728// ***************************************************29// For easier local testing, you can add to any case:30// {31// skip: true,32// --or--33// only: true,34// ...35// }36// ***************************************************3738// Tests that are valid/invalid across all parsers39const tests = {40 valid: [41 {42 code: normalizeIndent`43 function MyComponent() {44 const local = {};45 useEffect(() => {46 console.log(local);47 });48 }49 `,50 },51 {52 code: normalizeIndent`53 function MyComponent() {54 useEffect(() => {55 const local = {};56 console.log(local);57 }, []);58 }59 `,60 },61 {62 code: normalizeIndent`63 function MyComponent() {64 const local = someFunc();65 useEffect(() => {66 console.log(local);67 }, [local]);68 }69 `,70 },71 {72 // OK because `props` wasn't defined.73 // We don't technically know if `props` is supposed74 // to be an import that hasn't been added yet, or75 // a component-level variable. Ignore it until it76 // gets defined (a different rule would flag it anyway).77 code: normalizeIndent`78 function MyComponent() {79 useEffect(() => {80 console.log(props.foo);81 }, []);82 }83 `,84 },85 {86 code: normalizeIndent`87 function MyComponent() {88 const local1 = {};89 {90 const local2 = {};91 useEffect(() => {92 console.log(local1);93 console.log(local2);94 });95 }96 }97 `,98 },99 {100 code: normalizeIndent`101 function MyComponent() {102 const local1 = someFunc();103 {104 const local2 = someFunc();105 useCallback(() => {106 console.log(local1);107 console.log(local2);108 }, [local1, local2]);109 }110 }111 `,112 },113 {114 code: normalizeIndent`115 function MyComponent() {116 const local1 = someFunc();117 function MyNestedComponent() {118 const local2 = someFunc();119 useCallback(() => {120 console.log(local1);121 console.log(local2);122 }, [local2]);123 }124 }125 `,126 },127 {128 code: normalizeIndent`129 function MyComponent() {130 const local = someFunc();131 useEffect(() => {132 console.log(local);133 console.log(local);134 }, [local]);135 }136 `,137 },138 {139 code: normalizeIndent`140 function MyComponent() {141 useEffect(() => {142 console.log(unresolved);143 }, []);144 }145 `,146 },147 {148 code: normalizeIndent`149 function MyComponent() {150 const local = someFunc();151 useEffect(() => {152 console.log(local);153 }, [,,,local,,,]);154 }155 `,156 },157 {158 // Regression test159 code: normalizeIndent`160 function MyComponent({ foo }) {161 useEffect(() => {162 console.log(foo.length);163 }, [foo]);164 }165 `,166 },167 {168 // Regression test169 code: normalizeIndent`170 function MyComponent({ foo }) {171 useEffect(() => {172 console.log(foo.length);173 console.log(foo.slice(0));174 }, [foo]);175 }176 `,177 },178 {179 // Regression test180 code: normalizeIndent`181 function MyComponent({ history }) {182 useEffect(() => {183 return history.listen();184 }, [history]);185 }186 `,187 },188 {189 // Valid because they have meaning without deps.190 code: normalizeIndent`191 function MyComponent(props) {192 useEffect(() => {});193 useLayoutEffect(() => {});194 useImperativeHandle(props.innerRef, () => {});195 }196 `,197 },198 {199 code: normalizeIndent`200 function MyComponent(props) {201 useEffect(() => {202 console.log(props.foo);203 }, [props.foo]);204 }205 `,206 },207 {208 code: normalizeIndent`209 function MyComponent(props) {210 useEffect(() => {211 console.log(props.foo);212 console.log(props.bar);213 }, [props.bar, props.foo]);214 }215 `,216 },217 {218 code: normalizeIndent`219 function MyComponent(props) {220 useEffect(() => {221 console.log(props.foo);222 console.log(props.bar);223 }, [props.foo, props.bar]);224 }225 `,226 },227 {228 code: normalizeIndent`229 function MyComponent(props) {230 const local = someFunc();231 useEffect(() => {232 console.log(props.foo);233 console.log(props.bar);234 console.log(local);235 }, [props.foo, props.bar, local]);236 }237 `,238 },239 {240 // [props, props.foo] is technically unnecessary ('props' covers 'props.foo').241 // However, it's valid for effects to over-specify their deps.242 // So we don't warn about this. We *would* warn about useMemo/useCallback.243 code: normalizeIndent`244 function MyComponent(props) {245 const local = {};246 useEffect(() => {247 console.log(props.foo);248 console.log(props.bar);249 }, [props, props.foo]);250251 let color = someFunc();252 useEffect(() => {253 console.log(props.foo.bar.baz);254 console.log(color);255 }, [props.foo, props.foo.bar.baz, color]);256 }257 `,258 },259 // Nullish coalescing and optional chaining260 {261 code: normalizeIndent`262 function MyComponent(props) {263 useEffect(() => {264 console.log(props.foo?.bar?.baz ?? null);265 }, [props.foo]);266 }267 `,268 },269 {270 code: normalizeIndent`271 function MyComponent(props) {272 useEffect(() => {273 console.log(props.foo?.bar);274 }, [props.foo?.bar]);275 }276 `,277 },278 {279 code: normalizeIndent`280 function MyComponent(props) {281 useEffect(() => {282 console.log(props.foo?.bar);283 }, [props.foo.bar]);284 }285 `,286 },287 {288 code: normalizeIndent`289 function MyComponent(props) {290 useEffect(() => {291 console.log(props.foo.bar);292 }, [props.foo?.bar]);293 }294 `,295 },296 {297 code: normalizeIndent`298 function MyComponent(props) {299 useEffect(() => {300 console.log(props.foo.bar);301 console.log(props.foo?.bar);302 }, [props.foo?.bar]);303 }304 `,305 },306 {307 code: normalizeIndent`308 function MyComponent(props) {309 useEffect(() => {310 console.log(props.foo.bar);311 console.log(props.foo?.bar);312 }, [props.foo.bar]);313 }314 `,315 },316 {317 code: normalizeIndent`318 function MyComponent(props) {319 useEffect(() => {320 console.log(props.foo);321 console.log(props.foo?.bar);322 }, [props.foo]);323 }324 `,325 },326 {327 code: normalizeIndent`328 function MyComponent(props) {329 useEffect(() => {330 console.log(props.foo?.toString());331 }, [props.foo]);332 }333 `,334 },335 {336 code: normalizeIndent`337 function MyComponent(props) {338 useMemo(() => {339 console.log(props.foo?.toString());340 }, [props.foo]);341 }342 `,343 },344 {345 code: normalizeIndent`346 function MyComponent(props) {347 useCallback(() => {348 console.log(props.foo?.toString());349 }, [props.foo]);350 }351 `,352 },353 {354 code: normalizeIndent`355 function MyComponent(props) {356 useCallback(() => {357 console.log(props.foo.bar?.toString());358 }, [props.foo.bar]);359 }360 `,361 },362 {363 code: normalizeIndent`364 function MyComponent(props) {365 useCallback(() => {366 console.log(props.foo?.bar?.toString());367 }, [props.foo.bar]);368 }369 `,370 },371 {372 code: normalizeIndent`373 function MyComponent(props) {374 useCallback(() => {375 console.log(props.foo.bar.toString());376 }, [props?.foo?.bar]);377 }378 `,379 },380 {381 code: normalizeIndent`382 function MyComponent(props) {383 useCallback(() => {384 console.log(props.foo?.bar?.baz);385 }, [props?.foo.bar?.baz]);386 }387 `,388 },389 {390 code: normalizeIndent`391 function MyComponent() {392 const myEffect = () => {393 // Doesn't use anything394 };395 useEffect(myEffect, []);396 }397 `,398 },399 {400 code: normalizeIndent`401 const local = {};402 function MyComponent() {403 const myEffect = () => {404 console.log(local);405 };406 useEffect(myEffect, []);407 }408 `,409 },410 {411 code: normalizeIndent`412 const local = {};413 function MyComponent() {414 function myEffect() {415 console.log(local);416 }417 useEffect(myEffect, []);418 }419 `,420 },421 {422 code: normalizeIndent`423 function MyComponent() {424 const local = someFunc();425 function myEffect() {426 console.log(local);427 }428 useEffect(myEffect, [local]);429 }430 `,431 },432 {433 code: normalizeIndent`434 function MyComponent() {435 function myEffect() {436 console.log(global);437 }438 useEffect(myEffect, []);439 }440 `,441 },442 {443 code: normalizeIndent`444 const local = {};445 function MyComponent() {446 const myEffect = () => {447 otherThing()448 }449 const otherThing = () => {450 console.log(local);451 }452 useEffect(myEffect, []);453 }454 `,455 },456 {457 // Valid because even though we don't inspect the function itself,458 // at least it's passed as a dependency.459 code: normalizeIndent`460 function MyComponent({delay}) {461 const local = {};462 const myEffect = debounce(() => {463 console.log(local);464 }, delay);465 useEffect(myEffect, [myEffect]);466 }467 `,468 },469 {470 code: normalizeIndent`471 function MyComponent({myEffect}) {472 useEffect(myEffect, [,myEffect]);473 }474 `,475 },476 {477 code: normalizeIndent`478 function MyComponent({myEffect}) {479 useEffect(myEffect, [,myEffect,,]);480 }481 `,482 },483 {484 code: normalizeIndent`485 let local = {};486 function myEffect() {487 console.log(local);488 }489 function MyComponent() {490 useEffect(myEffect, []);491 }492 `,493 },494 {495 code: normalizeIndent`496 function MyComponent({myEffect}) {497 useEffect(myEffect, [myEffect]);498 }499 `,500 },501 {502 // Valid because has no deps.503 code: normalizeIndent`504 function MyComponent({myEffect}) {505 useEffect(myEffect);506 }507 `,508 },509 {510 code: normalizeIndent`511 function MyComponent(props) {512 useCustomEffect(() => {513 console.log(props.foo);514 });515 }516 `,517 options: [{additionalHooks: 'useCustomEffect'}],518 },519 {520 // behaves like no deps521 code: normalizeIndent`522 function MyComponent(props) {523 useSpecialEffect(() => {524 console.log(props.foo);525 }, null);526 }527 `,528 options: [529 {530 additionalHooks: 'useSpecialEffect',531 experimental_autoDependenciesHooks: ['useSpecialEffect'],532 },533 ],534 },535 {536 code: normalizeIndent`537 function MyComponent(props) {538 useCustomEffect(() => {539 console.log(props.foo);540 }, [props.foo]);541 }542 `,543 options: [{additionalHooks: 'useCustomEffect'}],544 },545 {546 code: normalizeIndent`547 function MyComponent(props) {548 useCustomEffect(() => {549 console.log(props.foo);550 }, []);551 }552 `,553 options: [{additionalHooks: 'useAnotherEffect'}],554 },555 {556 code: normalizeIndent`557 function MyComponent(props) {558 useWithoutEffectSuffix(() => {559 console.log(props.foo);560 }, []);561 }562 `,563 },564 {565 code: normalizeIndent`566 function MyComponent(props) {567 return renderHelperConfusedWithEffect(() => {568 console.log(props.foo);569 }, []);570 }571 `,572 },573 {574 // Valid because we don't care about hooks outside of components.575 code: normalizeIndent`576 const local = {};577 useEffect(() => {578 console.log(local);579 }, []);580 `,581 },582 {583 // Valid because we don't care about hooks outside of components.584 code: normalizeIndent`585 const local1 = {};586 {587 const local2 = {};588 useEffect(() => {589 console.log(local1);590 console.log(local2);591 }, []);592 }593 `,594 },595 {596 code: normalizeIndent`597 function MyComponent() {598 const ref = useRef();599 useEffect(() => {600 console.log(ref.current);601 }, [ref]);602 }603 `,604 },605 {606 code: normalizeIndent`607 function MyComponent() {608 const ref = useRef();609 useEffect(() => {610 console.log(ref.current);611 }, []);612 }613 `,614 },615 {616 code: normalizeIndent`617 function MyComponent({ maybeRef2, foo }) {618 const definitelyRef1 = useRef();619 const definitelyRef2 = useRef();620 const maybeRef1 = useSomeOtherRefyThing();621 const [state1, setState1] = useState();622 const [state2, setState2] = React.useState();623 const [state3, dispatch1] = useReducer();624 const [state4, dispatch2] = React.useReducer();625 const [state5, maybeSetState] = useFunnyState();626 const [state6, maybeDispatch] = useFunnyReducer();627 const [state9, dispatch5] = useActionState();628 const [state10, dispatch6] = React.useActionState();629 const [isPending1] = useTransition();630 const [isPending2, startTransition2] = useTransition();631 const [isPending3] = React.useTransition();632 const [isPending4, startTransition4] = React.useTransition();633 const mySetState = useCallback(() => {}, []);634 let myDispatch = useCallback(() => {}, []);635636 useEffect(() => {637 // Known to be static638 console.log(definitelyRef1.current);639 console.log(definitelyRef2.current);640 console.log(maybeRef1.current);641 console.log(maybeRef2.current);642 setState1();643 setState2();644 dispatch1();645 dispatch2();646 dispatch5();647 dispatch6();648 startTransition1();649 startTransition2();650 startTransition3();651 startTransition4();652653 // Dynamic654 console.log(state1);655 console.log(state2);656 console.log(state3);657 console.log(state4);658 console.log(state5);659 console.log(state6);660 console.log(isPending2);661 console.log(isPending4);662 mySetState();663 myDispatch();664665 // Not sure; assume dynamic666 maybeSetState();667 maybeDispatch();668 }, [669 // Dynamic670 state1, state2, state3, state4, state5, state6, state9, state10,671 maybeRef1, maybeRef2,672 isPending2, isPending4,673674 // Not sure; assume dynamic675 mySetState, myDispatch,676 maybeSetState, maybeDispatch677678 // In this test, we don't specify static deps.679 // That should be okay.680 ]);681 }682 `,683 },684 {685 code: normalizeIndent`686 function MyComponent({ maybeRef2 }) {687 const definitelyRef1 = useRef();688 const definitelyRef2 = useRef();689 const maybeRef1 = useSomeOtherRefyThing();690691 const [state1, setState1] = useState();692 const [state2, setState2] = React.useState();693 const [state3, dispatch1] = useReducer();694 const [state4, dispatch2] = React.useReducer();695696 const [state5, maybeSetState] = useFunnyState();697 const [state6, maybeDispatch] = useFunnyReducer();698699 const mySetState = useCallback(() => {}, []);700 let myDispatch = useCallback(() => {}, []);701702 useEffect(() => {703 // Known to be static704 console.log(definitelyRef1.current);705 console.log(definitelyRef2.current);706 console.log(maybeRef1.current);707 console.log(maybeRef2.current);708 setState1();709 setState2();710 dispatch1();711 dispatch2();712713 // Dynamic714 console.log(state1);715 console.log(state2);716 console.log(state3);717 console.log(state4);718 console.log(state5);719 console.log(state6);720 mySetState();721 myDispatch();722723 // Not sure; assume dynamic724 maybeSetState();725 maybeDispatch();726 }, [727 // Dynamic728 state1, state2, state3, state4, state5, state6,729 maybeRef1, maybeRef2,730731 // Not sure; assume dynamic732 mySetState, myDispatch,733 maybeSetState, maybeDispatch,734735 // In this test, we specify static deps.736 // That should be okay too!737 definitelyRef1, definitelyRef2, setState1, setState2, dispatch1, dispatch2738 ]);739 }740 `,741 },742 {743 code: normalizeIndent`744 const MyComponent = forwardRef((props, ref) => {745 useImperativeHandle(ref, () => ({746 focus() {747 alert(props.hello);748 }749 }))750 });751 `,752 },753 {754 code: normalizeIndent`755 const MyComponent = forwardRef((props, ref) => {756 useImperativeHandle(ref, () => ({757 focus() {758 alert(props.hello);759 }760 }), [props.hello])761 });762 `,763 },764 {765 // This is not ideal but warning would likely create766 // too many false positives. We do, however, prevent767 // direct assignments.768 code: normalizeIndent`769 function MyComponent(props) {770 let obj = someFunc();771 useEffect(() => {772 obj.foo = true;773 }, [obj]);774 }775 `,776 },777 {778 code: normalizeIndent`779 function MyComponent(props) {780 let foo = {}781 useEffect(() => {782 foo.bar.baz = 43;783 }, [foo.bar]);784 }785 `,786 },787 {788 // Valid because we assign ref.current789 // ourselves. Therefore it's likely not790 // a ref managed by React.791 code: normalizeIndent`792 function MyComponent() {793 const myRef = useRef();794 useEffect(() => {795 const handleMove = () => {};796 myRef.current = {};797 return () => {798 console.log(myRef.current.toString())799 };800 }, []);801 return <div />;802 }803 `,804 },805 {806 // Valid because we assign ref.current807 // ourselves. Therefore it's likely not808 // a ref managed by React.809 code: normalizeIndent`810 function MyComponent() {811 const myRef = useRef();812 useEffect(() => {813 const handleMove = () => {};814 myRef.current = {};815 return () => {816 console.log(myRef?.current?.toString())817 };818 }, []);819 return <div />;820 }821 `,822 },823 {824 // Valid because we assign ref.current825 // ourselves. Therefore it's likely not826 // a ref managed by React.827 code: normalizeIndent`828 function useMyThing(myRef) {829 useEffect(() => {830 const handleMove = () => {};831 myRef.current = {};832 return () => {833 console.log(myRef.current.toString())834 };835 }, [myRef]);836 }837 `,838 },839 {840 // Valid because the ref is captured.841 code: normalizeIndent`842 function MyComponent() {843 const myRef = useRef();844 useEffect(() => {845 const handleMove = () => {};846 const node = myRef.current;847 node.addEventListener('mousemove', handleMove);848 return () => node.removeEventListener('mousemove', handleMove);849 }, []);850 return <div ref={myRef} />;851 }852 `,853 },854 {855 // Valid because the ref is captured.856 code: normalizeIndent`857 function useMyThing(myRef) {858 useEffect(() => {859 const handleMove = () => {};860 const node = myRef.current;861 node.addEventListener('mousemove', handleMove);862 return () => node.removeEventListener('mousemove', handleMove);863 }, [myRef]);864 return <div ref={myRef} />;865 }866 `,867 },868 {869 // Valid because it's not an effect.870 code: normalizeIndent`871 function useMyThing(myRef) {872 useCallback(() => {873 const handleMouse = () => {};874 myRef.current.addEventListener('mousemove', handleMouse);875 myRef.current.addEventListener('mousein', handleMouse);876 return function() {877 setTimeout(() => {878 myRef.current.removeEventListener('mousemove', handleMouse);879 myRef.current.removeEventListener('mousein', handleMouse);880 });881 }882 }, [myRef]);883 }884 `,885 },886 {887 // Valid because we read ref.current in a function that isn't cleanup.888 code: normalizeIndent`889 function useMyThing() {890 const myRef = useRef();891 useEffect(() => {892 const handleMove = () => {893 console.log(myRef.current)894 };895 window.addEventListener('mousemove', handleMove);896 return () => window.removeEventListener('mousemove', handleMove);897 }, []);898 return <div ref={myRef} />;899 }900 `,901 },902 {903 // Valid because we read ref.current in a function that isn't cleanup.904 code: normalizeIndent`905 function useMyThing() {906 const myRef = useRef();907 useEffect(() => {908 const handleMove = () => {909 return () => window.removeEventListener('mousemove', handleMove);910 };911 window.addEventListener('mousemove', handleMove);912 return () => {};913 }, []);914 return <div ref={myRef} />;915 }916 `,917 },918 {919 // Valid because it's a primitive constant.920 code: normalizeIndent`921 function MyComponent() {922 const local1 = 42;923 const local2 = '42';924 const local3 = null;925 useEffect(() => {926 console.log(local1);927 console.log(local2);928 console.log(local3);929 }, []);930 }931 `,932 },933 {934 // It's not a mistake to specify constant values though.935 code: normalizeIndent`936 function MyComponent() {937 const local1 = 42;938 const local2 = '42';939 const local3 = null;940 useEffect(() => {941 console.log(local1);942 console.log(local2);943 console.log(local3);944 }, [local1, local2, local3]);945 }946 `,947 },948 {949 // It is valid for effects to over-specify their deps.950 code: normalizeIndent`951 function MyComponent(props) {952 const local = props.local;953 useEffect(() => {}, [local]);954 }955 `,956 },957 {958 // Valid even though activeTab is "unused".959 // We allow over-specifying deps for effects, but not callbacks or memo.960 code: normalizeIndent`961 function Foo({ activeTab }) {962 useEffect(() => {963 window.scrollTo(0, 0);964 }, [activeTab]);965 }966 `,967 },968 {969 // It is valid to specify broader effect deps than strictly necessary.970 // Don't warn for this.971 code: normalizeIndent`972 function MyComponent(props) {973 useEffect(() => {974 console.log(props.foo.bar.baz);975 }, [props]);976 useEffect(() => {977 console.log(props.foo.bar.baz);978 }, [props.foo]);979 useEffect(() => {980 console.log(props.foo.bar.baz);981 }, [props.foo.bar]);982 useEffect(() => {983 console.log(props.foo.bar.baz);984 }, [props.foo.bar.baz]);985 }986 `,987 },988 {989 // It is *also* valid to specify broader memo/callback deps than strictly necessary.990 // Don't warn for this either.991 code: normalizeIndent`992 function MyComponent(props) {993 const fn = useCallback(() => {994 console.log(props.foo.bar.baz);995 }, [props]);996 const fn2 = useCallback(() => {997 console.log(props.foo.bar.baz);998 }, [props.foo]);999 const fn3 = useMemo(() => {1000 console.log(props.foo.bar.baz);1001 }, [props.foo.bar]);1002 const fn4 = useMemo(() => {1003 console.log(props.foo.bar.baz);1004 }, [props.foo.bar.baz]);1005 }1006 `,1007 },1008 {1009 // Declaring handleNext is optional because1010 // it doesn't use anything in the function scope.1011 code: normalizeIndent`1012 function MyComponent(props) {1013 function handleNext1() {1014 console.log('hello');1015 }1016 const handleNext2 = () => {1017 console.log('hello');1018 };1019 let handleNext3 = function() {1020 console.log('hello');1021 };1022 useEffect(() => {1023 return Store.subscribe(handleNext1);1024 }, []);1025 useLayoutEffect(() => {1026 return Store.subscribe(handleNext2);1027 }, []);1028 useMemo(() => {1029 return Store.subscribe(handleNext3);1030 }, []);1031 }1032 `,1033 },1034 {1035 // Declaring handleNext is optional because1036 // it doesn't use anything in the function scope.1037 code: normalizeIndent`1038 function MyComponent(props) {1039 function handleNext() {1040 console.log('hello');1041 }1042 useEffect(() => {1043 return Store.subscribe(handleNext);1044 }, []);1045 useLayoutEffect(() => {1046 return Store.subscribe(handleNext);1047 }, []);1048 useMemo(() => {1049 return Store.subscribe(handleNext);1050 }, []);1051 }1052 `,1053 },1054 {1055 // Declaring handleNext is optional because1056 // everything they use is fully static.1057 code: normalizeIndent`1058 function MyComponent(props) {1059 let [, setState] = useState();1060 let [, dispatch] = React.useReducer();10611062 function handleNext1(value) {1063 let value2 = value * 100;1064 setState(value2);1065 console.log('hello');1066 }1067 const handleNext2 = (value) => {1068 setState(foo(value));1069 console.log('hello');1070 };1071 let handleNext3 = function(value) {1072 console.log(value);1073 dispatch({ type: 'x', value });1074 };1075 useEffect(() => {1076 return Store.subscribe(handleNext1);1077 }, []);1078 useLayoutEffect(() => {1079 return Store.subscribe(handleNext2);1080 }, []);1081 useMemo(() => {1082 return Store.subscribe(handleNext3);1083 }, []);1084 }1085 `,1086 },1087 {1088 code: normalizeIndent`1089 function useInterval(callback, delay) {1090 const savedCallback = useRef();1091 useEffect(() => {1092 savedCallback.current = callback;1093 });1094 useEffect(() => {1095 function tick() {1096 savedCallback.current();1097 }1098 if (delay !== null) {1099 let id = setInterval(tick, delay);1100 return () => clearInterval(id);1101 }1102 }, [delay]);1103 }1104 `,1105 },1106 {1107 code: normalizeIndent`1108 function Counter() {1109 const [count, setCount] = useState(0);11101111 useEffect(() => {1112 let id = setInterval(() => {1113 setCount(c => c + 1);1114 }, 1000);1115 return () => clearInterval(id);1116 }, []);11171118 return <h1>{count}</h1>;1119 }1120 `,1121 },1122 {1123 code: normalizeIndent`1124 function Counter(unstableProp) {1125 let [count, setCount] = useState(0);1126 setCount = unstableProp1127 useEffect(() => {1128 let id = setInterval(() => {1129 setCount(c => c + 1);1130 }, 1000);1131 return () => clearInterval(id);1132 }, [setCount]);11331134 return <h1>{count}</h1>;1135 }1136 `,1137 },1138 {1139 code: normalizeIndent`1140 function Counter() {1141 const [count, setCount] = useState(0);11421143 function tick() {1144 setCount(c => c + 1);1145 }11461147 useEffect(() => {1148 let id = setInterval(() => {1149 tick();1150 }, 1000);1151 return () => clearInterval(id);1152 }, []);11531154 return <h1>{count}</h1>;1155 }1156 `,1157 },1158 {1159 code: normalizeIndent`1160 function Counter() {1161 const [count, dispatch] = useReducer((state, action) => {1162 if (action === 'inc') {1163 return state + 1;1164 }1165 }, 0);11661167 useEffect(() => {1168 let id = setInterval(() => {1169 dispatch('inc');1170 }, 1000);1171 return () => clearInterval(id);1172 }, []);11731174 return <h1>{count}</h1>;1175 }1176 `,1177 },1178 {1179 code: normalizeIndent`1180 function Counter() {1181 const [count, dispatch] = useReducer((state, action) => {1182 if (action === 'inc') {1183 return state + 1;1184 }1185 }, 0);11861187 const tick = () => {1188 dispatch('inc');1189 };11901191 useEffect(() => {1192 let id = setInterval(tick, 1000);1193 return () => clearInterval(id);1194 }, []);11951196 return <h1>{count}</h1>;1197 }1198 `,1199 },1200 {1201 // Regression test for a crash1202 code: normalizeIndent`1203 function Podcasts() {1204 useEffect(() => {1205 setPodcasts([]);1206 }, []);1207 let [podcasts, setPodcasts] = useState(null);1208 }1209 `,1210 },1211 {1212 code: normalizeIndent`1213 function withFetch(fetchPodcasts) {1214 return function Podcasts({ id }) {1215 let [podcasts, setPodcasts] = useState(null);1216 useEffect(() => {1217 fetchPodcasts(id).then(setPodcasts);1218 }, [id]);1219 }1220 }1221 `,1222 },1223 {1224 code: normalizeIndent`1225 function Podcasts({ id }) {1226 let [podcasts, setPodcasts] = useState(null);1227 useEffect(() => {1228 function doFetch({ fetchPodcasts }) {1229 fetchPodcasts(id).then(setPodcasts);1230 }1231 doFetch({ fetchPodcasts: API.fetchPodcasts });1232 }, [id]);1233 }1234 `,1235 },1236 {1237 code: normalizeIndent`1238 function Counter() {1239 let [count, setCount] = useState(0);12401241 function increment(x) {1242 return x + 1;1243 }12441245 useEffect(() => {1246 let id = setInterval(() => {1247 setCount(increment);1248 }, 1000);1249 return () => clearInterval(id);1250 }, []);12511252 return <h1>{count}</h1>;1253 }1254 `,1255 },1256 {1257 code: normalizeIndent`1258 function Counter() {1259 let [count, setCount] = useState(0);12601261 function increment(x) {1262 return x + 1;1263 }12641265 useEffect(() => {1266 let id = setInterval(() => {1267 setCount(count => increment(count));1268 }, 1000);1269 return () => clearInterval(id);1270 }, []);12711272 return <h1>{count}</h1>;1273 }1274 `,1275 },1276 {1277 code: normalizeIndent`1278 import increment from './increment';1279 function Counter() {1280 let [count, setCount] = useState(0);12811282 useEffect(() => {1283 let id = setInterval(() => {1284 setCount(count => count + increment);1285 }, 1000);1286 return () => clearInterval(id);1287 }, []);12881289 return <h1>{count}</h1>;1290 }1291 `,1292 },1293 {1294 code: normalizeIndent`1295 function withStuff(increment) {1296 return function Counter() {1297 let [count, setCount] = useState(0);12981299 useEffect(() => {1300 let id = setInterval(() => {1301 setCount(count => count + increment);1302 }, 1000);1303 return () => clearInterval(id);1304 }, []);13051306 return <h1>{count}</h1>;1307 }1308 }1309 `,1310 },1311 {1312 code: normalizeIndent`1313 function App() {1314 const [query, setQuery] = useState('react');1315 const [state, setState] = useState(null);1316 useEffect(() => {1317 let ignore = false;1318 fetchSomething();1319 async function fetchSomething() {1320 const result = await (await fetch('http://hn.algolia.com/api/v1/search?query=' + query)).json();1321 if (!ignore) setState(result);1322 }1323 return () => { ignore = true; };1324 }, [query]);1325 return (1326 <>1327 <input value={query} onChange={e => setQuery(e.target.value)} />1328 {JSON.stringify(state)}1329 </>1330 );1331 }1332 `,1333 },1334 {1335 code: normalizeIndent`1336 function Example() {1337 const foo = useCallback(() => {1338 foo();1339 }, []);1340 }1341 `,1342 },1343 {1344 code: normalizeIndent`1345 function Example({ prop }) {1346 const foo = useCallback(() => {1347 if (prop) {1348 foo();1349 }1350 }, [prop]);1351 }1352 `,1353 },1354 {1355 code: normalizeIndent`1356 function Hello() {1357 const [state, setState] = useState(0);1358 useEffect(() => {1359 const handleResize = () => setState(window.innerWidth);1360 window.addEventListener('resize', handleResize);1361 return () => window.removeEventListener('resize', handleResize);1362 });1363 }1364 `,1365 },1366 // Ignore arguments keyword for arrow functions.1367 {1368 code: normalizeIndent`1369 function Example() {1370 useEffect(() => {1371 arguments1372 }, [])1373 }1374 `,1375 },1376 {1377 code: normalizeIndent`1378 function Example() {1379 useEffect(() => {1380 const bar = () => {1381 arguments;1382 };1383 bar();1384 }, [])1385 }1386 `,1387 },1388 // Regression test.1389 {1390 code: normalizeIndent`1391 function Example(props) {1392 useEffect(() => {1393 let topHeight = 0;1394 topHeight = props.upperViewHeight;1395 }, [props.upperViewHeight]);1396 }1397 `,1398 },1399 // Regression test.1400 {1401 code: normalizeIndent`1402 function Example(props) {1403 useEffect(() => {1404 let topHeight = 0;1405 topHeight = props?.upperViewHeight;1406 }, [props?.upperViewHeight]);1407 }1408 `,1409 },1410 // Regression test.1411 {1412 code: normalizeIndent`1413 function Example(props) {1414 useEffect(() => {1415 let topHeight = 0;1416 topHeight = props?.upperViewHeight;1417 }, [props]);1418 }1419 `,1420 },1421 {1422 code: normalizeIndent`1423 function useFoo(foo){1424 return useMemo(() => foo, [foo]);1425 }1426 `,1427 },1428 {1429 code: normalizeIndent`1430 function useFoo(){1431 const foo = "hi!";1432 return useMemo(() => foo, [foo]);1433 }1434 `,1435 },1436 {1437 code: normalizeIndent`1438 function useFoo(){1439 let {foo} = {foo: 1};1440 return useMemo(() => foo, [foo]);1441 }1442 `,1443 },1444 {1445 code: normalizeIndent`1446 function useFoo(){1447 let [foo] = [1];1448 return useMemo(() => foo, [foo]);1449 }1450 `,1451 },1452 {1453 code: normalizeIndent`1454 function useFoo() {1455 const foo = "fine";1456 if (true) {1457 // Shadowed variable with constant construction in a nested scope is fine.1458 const foo = {};1459 }1460 return useMemo(() => foo, [foo]);1461 }1462 `,1463 },1464 {1465 code: normalizeIndent`1466 function MyComponent({foo}) {1467 return useMemo(() => foo, [foo])1468 }1469 `,1470 },1471 {1472 code: normalizeIndent`1473 function MyComponent() {1474 const foo = true ? "fine" : "also fine";1475 return useMemo(() => foo, [foo]);1476 }1477 `,1478 },1479 {1480 code: normalizeIndent`1481 function MyComponent() {1482 useEffect(() => {1483 console.log('banana banana banana');1484 }, undefined);1485 }1486 `,1487 },1488 {1489 // Test settings-based additionalHooks - should work with settings1490 code: normalizeIndent`1491 function MyComponent(props) {1492 useCustomEffect(() => {1493 console.log(props.foo);1494 });1495 }1496 `,1497 settings: {1498 'react-hooks': {1499 additionalEffectHooks: 'useCustomEffect',1500 },1501 },1502 },1503 {1504 // Test settings-based additionalHooks - should work with dependencies1505 code: normalizeIndent`1506 function MyComponent(props) {1507 useCustomEffect(() => {1508 console.log(props.foo);1509 }, [props.foo]);1510 }1511 `,1512 settings: {1513 'react-hooks': {1514 additionalEffectHooks: 'useCustomEffect',1515 },1516 },1517 },1518 {1519 // Test that rule-level additionalHooks takes precedence over settings1520 code: normalizeIndent`1521 function MyComponent(props) {1522 useCustomEffect(() => {1523 console.log(props.foo);1524 }, []);1525 }1526 `,1527 options: [{additionalHooks: 'useAnotherEffect'}],1528 settings: {1529 'react-hooks': {1530 additionalEffectHooks: 'useCustomEffect',1531 },1532 },1533 },1534 {1535 // Test settings with multiple hooks pattern1536 code: normalizeIndent`1537 function MyComponent(props) {1538 useCustomEffect(() => {1539 console.log(props.foo);1540 }, [props.foo]);1541 useAnotherEffect(() => {1542 console.log(props.bar);1543 }, [props.bar]);1544 }1545 `,1546 settings: {1547 'react-hooks': {1548 additionalEffectHooks: '(useCustomEffect|useAnotherEffect)',1549 },1550 },1551 },1552 {1553 code: normalizeIndent`1554 function MyComponent({ theme }) {1555 const onStuff = useEffectEvent(() => {1556 showNotification(theme);1557 });1558 useEffect(() => {1559 onStuff();1560 }, []);1561 React.useEffect(() => {1562 onStuff();1563 }, []);1564 }1565 `,1566 },1567 ],1568 invalid: [1569 {1570 code: normalizeIndent`1571 function MyComponent(props) {1572 useSpecialEffect(() => {1573 console.log(props.foo);1574 }, null);1575 }1576 `,1577 options: [{additionalHooks: 'useSpecialEffect'}],1578 errors: [1579 {1580 message:1581 "React Hook useSpecialEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies.",1582 },1583 {1584 message:1585 "React Hook useSpecialEffect has a missing dependency: 'props.foo'. Either include it or remove the dependency array.",1586 suggestions: [1587 {1588 desc: 'Update the dependencies array to be: [props.foo]',1589 output: normalizeIndent`1590 function MyComponent(props) {1591 useSpecialEffect(() => {1592 console.log(props.foo);1593 }, [props.foo]);1594 }1595 `,1596 },1597 ],1598 },1599 ],1600 },1601 {1602 code: normalizeIndent`1603 function MyComponent(props) {1604 useCallback(() => {1605 console.log(props.foo?.toString());1606 }, []);1607 }1608 `,1609 errors: [1610 {1611 message:1612 "React Hook useCallback has a missing dependency: 'props.foo'. " +1613 'Either include it or remove the dependency array.',1614 suggestions: [1615 {1616 desc: 'Update the dependencies array to be: [props.foo]',1617 output: normalizeIndent`1618 function MyComponent(props) {1619 useCallback(() => {1620 console.log(props.foo?.toString());1621 }, [props.foo]);1622 }1623 `,1624 },1625 ],1626 },1627 ],1628 },1629 {1630 // Affected code should use React.useActionState instead1631 code: normalizeIndent`1632 function ComponentUsingFormState(props) {1633 const [state7, dispatch3] = useFormState();1634 const [state8, dispatch4] = ReactDOM.useFormState();1635 useEffect(() => {1636 dispatch3();1637 dispatch4();16381639 // dynamic1640 console.log(state7);1641 console.log(state8);16421643 }, [state7, state8]);1644 }1645 `,1646 errors: [1647 {1648 message:1649 "React Hook useEffect has missing dependencies: 'dispatch3' and 'dispatch4'. " +1650 'Either include them or remove the dependency array.',1651 suggestions: [1652 {1653 desc: 'Update the dependencies array to be: [dispatch3, dispatch4, state7, state8]',1654 output: normalizeIndent`1655 function ComponentUsingFormState(props) {1656 const [state7, dispatch3] = useFormState();1657 const [state8, dispatch4] = ReactDOM.useFormState();1658 useEffect(() => {1659 dispatch3();1660 dispatch4();16611662 // dynamic1663 console.log(state7);1664 console.log(state8);16651666 }, [dispatch3, dispatch4, state7, state8]);1667 }1668 `,1669 },1670 ],1671 },1672 ],1673 },1674 {1675 code: normalizeIndent`1676 function MyComponent(props) {1677 useCallback(() => {1678 console.log(props.foo?.bar.baz);1679 }, []);1680 }1681 `,1682 errors: [1683 {1684 message:1685 "React Hook useCallback has a missing dependency: 'props.foo?.bar.baz'. " +1686 'Either include it or remove the dependency array.',1687 suggestions: [1688 {1689 desc: 'Update the dependencies array to be: [props.foo?.bar.baz]',1690 output: normalizeIndent`1691 function MyComponent(props) {1692 useCallback(() => {1693 console.log(props.foo?.bar.baz);1694 }, [props.foo?.bar.baz]);1695 }1696 `,1697 },1698 ],1699 },1700 ],1701 },1702 {1703 code: normalizeIndent`1704 function MyComponent(props) {1705 useCallback(() => {1706 console.log(props.foo?.bar?.baz);1707 }, []);1708 }1709 `,1710 errors: [1711 {1712 message:1713 "React Hook useCallback has a missing dependency: 'props.foo?.bar?.baz'. " +1714 'Either include it or remove the dependency array.',1715 suggestions: [1716 {1717 desc: 'Update the dependencies array to be: [props.foo?.bar?.baz]',1718 output: normalizeIndent`1719 function MyComponent(props) {1720 useCallback(() => {1721 console.log(props.foo?.bar?.baz);1722 }, [props.foo?.bar?.baz]);1723 }1724 `,1725 },1726 ],1727 },1728 ],1729 },1730 {1731 code: normalizeIndent`1732 function MyComponent(props) {1733 useCallback(() => {1734 console.log(props.foo?.bar.toString());1735 }, []);1736 }1737 `,1738 errors: [1739 {1740 message:1741 "React Hook useCallback has a missing dependency: 'props.foo?.bar'. " +1742 'Either include it or remove the dependency array.',1743 suggestions: [1744 {1745 desc: 'Update the dependencies array to be: [props.foo?.bar]',1746 output: normalizeIndent`1747 function MyComponent(props) {1748 useCallback(() => {1749 console.log(props.foo?.bar.toString());1750 }, [props.foo?.bar]);1751 }1752 `,1753 },1754 ],1755 },1756 ],1757 },1758 {1759 code: normalizeIndent`1760 function MyComponent() {1761 const local = someFunc();1762 useEffect(() => {1763 console.log(local);1764 }, []);1765 }1766 `,1767 errors: [1768 {1769 message:1770 "React Hook useEffect has a missing dependency: 'local'. " +1771 'Either include it or remove the dependency array.',1772 suggestions: [1773 {1774 desc: 'Update the dependencies array to be: [local]',1775 output: normalizeIndent`1776 function MyComponent() {1777 const local = someFunc();1778 useEffect(() => {1779 console.log(local);1780 }, [local]);1781 }1782 `,1783 },1784 ],1785 },1786 ],1787 },1788 {1789 code: normalizeIndent`1790 function Counter(unstableProp) {1791 let [count, setCount] = useState(0);1792 setCount = unstableProp1793 useEffect(() => {1794 let id = setInterval(() => {1795 setCount(c => c + 1);1796 }, 1000);1797 return () => clearInterval(id);1798 }, []);17991800 return <h1>{count}</h1>;1801 }1802 `,1803 errors: [1804 {1805 message:1806 "React Hook useEffect has a missing dependency: 'setCount'. " +1807 'Either include it or remove the dependency array.',1808 suggestions: [1809 {1810 desc: 'Update the dependencies array to be: [setCount]',1811 output: normalizeIndent`1812 function Counter(unstableProp) {1813 let [count, setCount] = useState(0);1814 setCount = unstableProp1815 useEffect(() => {1816 let id = setInterval(() => {1817 setCount(c => c + 1);1818 }, 1000);1819 return () => clearInterval(id);1820 }, [setCount]);18211822 return <h1>{count}</h1>;1823 }1824 `,1825 },1826 ],1827 },1828 ],1829 },1830 {1831 // Note: we *could* detect it's a primitive and never assigned1832 // even though it's not a constant -- but we currently don't.1833 // So this is an error.1834 code: normalizeIndent`1835 function MyComponent() {1836 let local = 42;1837 useEffect(() => {1838 console.log(local);1839 }, []);1840 }1841 `,1842 errors: [1843 {1844 message:1845 "React Hook useEffect has a missing dependency: 'local'. " +1846 'Either include it or remove the dependency array.',1847 suggestions: [1848 {1849 desc: 'Update the dependencies array to be: [local]',1850 output: normalizeIndent`1851 function MyComponent() {1852 let local = 42;1853 useEffect(() => {1854 console.log(local);1855 }, [local]);1856 }1857 `,1858 },1859 ],1860 },1861 ],1862 },1863 {1864 // Regexes are literals but potentially stateful.1865 code: normalizeIndent`1866 function MyComponent() {1867 const local = /foo/;1868 useEffect(() => {1869 console.log(local);1870 }, []);1871 }1872 `,1873 errors: [1874 {1875 message:1876 "React Hook useEffect has a missing dependency: 'local'. " +1877 'Either include it or remove the dependency array.',1878 suggestions: [1879 {1880 desc: 'Update the dependencies array to be: [local]',1881 output: normalizeIndent`1882 function MyComponent() {1883 const local = /foo/;1884 useEffect(() => {1885 console.log(local);1886 }, [local]);1887 }1888 `,1889 },1890 ],1891 },1892 ],1893 },1894 {1895 // Invalid because they don't have a meaning without deps.1896 code: normalizeIndent`1897 function MyComponent(props) {1898 const value = useMemo(() => { return 2*2; });1899 const fn = useCallback(() => { alert('foo'); });1900 }1901 `,1902 // We don't know what you meant.1903 errors: [1904 {1905 message:1906 'React Hook useMemo does nothing when called with only one argument. ' +1907 'Did you forget to pass an array of dependencies?',1908 suggestions: undefined,1909 },1910 {1911 message:1912 'React Hook useCallback does nothing when called with only one argument. ' +1913 'Did you forget to pass an array of dependencies?',1914 suggestions: undefined,1915 },1916 ],1917 },1918 {1919 // Invalid because they don't have a meaning without deps.1920 code: normalizeIndent`1921 function MyComponent({ fn1, fn2 }) {1922 const value = useMemo(fn1);1923 const fn = useCallback(fn2);1924 }1925 `,1926 errors: [1927 {1928 message:1929 'React Hook useMemo does nothing when called with only one argument. ' +1930 'Did you forget to pass an array of dependencies?',1931 suggestions: undefined,1932 },1933 {1934 message:1935 'React Hook useCallback does nothing when called with only one argument. ' +1936 'Did you forget to pass an array of dependencies?',1937 suggestions: undefined,1938 },1939 ],1940 },1941 {1942 code: normalizeIndent`1943 function MyComponent() {1944 useEffect()1945 useLayoutEffect()1946 useCallback()1947 useMemo()1948 }1949 `,1950 errors: [1951 {1952 message:1953 'React Hook useEffect requires an effect callback. ' +1954 'Did you forget to pass a callback to the hook?',1955 suggestions: undefined,1956 },1957 {1958 message:1959 'React Hook useLayoutEffect requires an effect callback. ' +1960 'Did you forget to pass a callback to the hook?',1961 suggestions: undefined,1962 },1963 {1964 message:1965 'React Hook useCallback requires an effect callback. ' +1966 'Did you forget to pass a callback to the hook?',1967 suggestions: undefined,1968 },1969 {1970 message:1971 'React Hook useMemo requires an effect callback. ' +1972 'Did you forget to pass a callback to the hook?',1973 suggestions: undefined,1974 },1975 ],1976 },1977 {1978 // Regression test1979 code: normalizeIndent`1980 function MyComponent() {1981 const local = someFunc();1982 useEffect(() => {1983 if (true) {1984 console.log(local);1985 }1986 }, []);1987 }1988 `,1989 errors: [1990 {1991 message:1992 "React Hook useEffect has a missing dependency: 'local'. " +1993 'Either include it or remove the dependency array.',1994 suggestions: [1995 {1996 desc: 'Update the dependencies array to be: [local]',1997 output: normalizeIndent`1998 function MyComponent() {1999 const local = someFunc();2000 useEffect(() => {
Findings
✓ No findings reported for this file.