@samitouri / QOS-React / commits / 3ada82b741

Allow extraneous effect dependencies (#14967)

This makes cases like useEffect(() => { window.scrollTo(0, 0); }, [activeTab]); not warn. However, it still warns for unused useCallback/useMemo deps.

Dan Abramov committed Feb 27, 2019 at 16:59 UTC 3ada82b7416c51290535a054f345b99378c38dcb
2 files changed +99 -22
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+87 -20
@@ -95,7 +95,7 @@ const tests = {
95 const local1 = {};
96 {
97 const local2 = {};
98 - useEffect(() => {
98 + useCallback(() => {
99 console.log(local1);
100 console.log(local2);
101 }, [local1, local2]);
@@ -109,7 +109,7 @@ const tests = {
109 const local1 = {};
110 function MyNestedComponent() {
111 const local2 = {};
112 - useEffect(() => {
112 + useCallback(() => {
113 console.log(local1);
114 console.log(local2);
115 }, [local2]);
@@ -590,6 +590,17 @@ const tests = {
590 }
591 `,
592 },
593 + {
594 + // Valid even though activeTab is "unused".
595 + // We allow that in effects, but not callbacks or memo.
596 + code: `
597 + function Foo({ activeTab }) {
598 + useEffect(() => {
599 + window.scrollTo(0, 0);
600 + }, [activeTab]);
601 + }
602 + `,
603 + },
604 ],
605 invalid: [
606 {
@@ -809,7 +820,7 @@ const tests = {
820 function MyComponent() {
821 const local1 = {};
822 const local2 = {};
812 - useEffect(() => {
823 + useMemo(() => {
824 console.log(local1);
825 }, [local1, local2]);
826 }
@@ -818,13 +829,13 @@ const tests = {
829 function MyComponent() {
830 const local1 = {};
831 const local2 = {};
821 - useEffect(() => {
832 + useMemo(() => {
833 console.log(local1);
834 }, [local1]);
835 }
836 `,
837 errors: [
827 - "React Hook useEffect has an unnecessary dependency: 'local2'. " +
838 + "React Hook useMemo has an unnecessary dependency: 'local2'. " +
839 'Either exclude it or remove the dependency array.',
840 ],
841 },
@@ -836,7 +847,7 @@ const tests = {
847 const local1 = {};
848 function MyNestedComponent() {
849 const local2 = {};
839 - useEffect(() => {
850 + useCallback(() => {
851 console.log(local1);
852 console.log(local2);
853 }, [local1]);
@@ -848,7 +859,7 @@ const tests = {
859 const local1 = {};
860 function MyNestedComponent() {
861 const local2 = {};
851 - useEffect(() => {
862 + useCallback(() => {
863 console.log(local1);
864 console.log(local2);
865 }, [local2]);
@@ -857,7 +868,7 @@ const tests = {
868 `,
869 errors: [
870 // Focus on the more important part first (missing dep)
860 - "React Hook useEffect has a missing dependency: 'local2'. " +
871 + "React Hook useCallback has a missing dependency: 'local2'. " +
872 'Either include it or remove the dependency array.',
873 ],
874 },
@@ -912,16 +923,16 @@ const tests = {
923 {
924 code: `
925 function MyComponent() {
915 - useEffect(() => {}, [local]);
926 + useCallback(() => {}, [local]);
927 }
928 `,
929 output: `
930 function MyComponent() {
920 - useEffect(() => {}, []);
931 + useCallback(() => {}, []);
932 }
933 `,
934 errors: [
924 - "React Hook useEffect has an unnecessary dependency: 'local'. " +
935 + "React Hook useCallback has an unnecessary dependency: 'local'. " +
936 'Either exclude it or remove the dependency array.',
937 ],
938 },
@@ -1229,7 +1240,6 @@ const tests = {
1240 ],
1241 },
1242 {
1232 - // TODO: need to think more about this case.
1243 code: `
1244 function MyComponent() {
1245 const local = {id: 42};
@@ -1238,13 +1248,14 @@ const tests = {
1248 }, [local.id]);
1249 }
1250 `,
1241 - // TODO: this may not be a good idea.
1251 + // TODO: autofix should be smart enough
1252 + // to remove local.id from the list.
1253 output: `
1254 function MyComponent() {
1255 const local = {id: 42};
1256 useEffect(() => {
1257 console.log(local);
1247 - }, [local]);
1258 + }, [local, local.id]);
1259 }
1260 `,
1261 errors: [
@@ -1278,7 +1289,7 @@ const tests = {
1289 code: `
1290 function MyComponent() {
1291 const local1 = {};
1281 - useEffect(() => {
1292 + useCallback(() => {
1293 const local1 = {};
1294 console.log(local1);
1295 }, [local1]);
@@ -1287,14 +1298,14 @@ const tests = {
1298 output: `
1299 function MyComponent() {
1300 const local1 = {};
1290 - useEffect(() => {
1301 + useCallback(() => {
1302 const local1 = {};
1303 console.log(local1);
1304 }, []);
1305 }
1306 `,
1307 errors: [
1297 - "React Hook useEffect has an unnecessary dependency: 'local1'. " +
1308 + "React Hook useCallback has an unnecessary dependency: 'local1'. " +
1309 'Either exclude it or remove the dependency array.',
1310 ],
1311 },
@@ -1302,17 +1313,17 @@ const tests = {
1313 code: `
1314 function MyComponent() {
1315 const local1 = {};
1305 - useEffect(() => {}, [local1]);
1316 + useCallback(() => {}, [local1]);
1317 }
1318 `,
1319 output: `
1320 function MyComponent() {
1321 const local1 = {};
1311 - useEffect(() => {}, []);
1322 + useCallback(() => {}, []);
1323 }
1324 `,
1325 errors: [
1315 - "React Hook useEffect has an unnecessary dependency: 'local1'. " +
1326 + "React Hook useCallback has an unnecessary dependency: 'local1'. " +
1327 'Either exclude it or remove the dependency array.',
1328 ],
1329 },
@@ -1785,6 +1796,62 @@ const tests = {
1796 "because their mutation doesn't re-render the component.",
1797 ],
1798 },
1799 + {
1800 + code: `
1801 + function MyComponent({ activeTab }) {
1802 + const ref1 = useRef();
1803 + const ref2 = useRef();
1804 + useEffect(() => {
1805 + ref1.current.scrollTop = 0;
1806 + ref2.current.scrollTop = 0;
1807 + }, [ref1.current, ref2.current, activeTab]);
1808 + }
1809 + `,
1810 + output: `
1811 + function MyComponent({ activeTab }) {
1812 + const ref1 = useRef();
1813 + const ref2 = useRef();
1814 + useEffect(() => {
1815 + ref1.current.scrollTop = 0;
1816 + ref2.current.scrollTop = 0;
1817 + }, [activeTab]);
1818 + }
1819 + `,
1820 + errors: [
1821 + "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " +
1822 + 'Either exclude them or remove the dependency array. ' +
1823 + "Mutable values like 'ref1.current' aren't valid dependencies " +
1824 + "because their mutation doesn't re-render the component.",
1825 + ],
1826 + },
1827 + {
1828 + code: `
1829 + function MyComponent({ activeTab, initY }) {
1830 + const ref1 = useRef();
1831 + const ref2 = useRef();
1832 + const fn = useCallback(() => {
1833 + ref1.current.scrollTop = initY;
1834 + ref2.current.scrollTop = initY;
1835 + }, [ref1.current, ref2.current, activeTab, initY]);
1836 + }
1837 + `,
1838 + output: `
1839 + function MyComponent({ activeTab, initY }) {
1840 + const ref1 = useRef();
1841 + const ref2 = useRef();
1842 + const fn = useCallback(() => {
1843 + ref1.current.scrollTop = initY;
1844 + ref2.current.scrollTop = initY;
1845 + }, [initY]);
1846 + }
1847 + `,
1848 + errors: [
1849 + "React Hook useCallback has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " +
1850 + 'Either exclude them or remove the dependency array. ' +
1851 + "Mutable values like 'ref1.current' aren't valid dependencies " +
1852 + "because their mutation doesn't re-render the component.",
1853 + ],
1854 + },
1855 {
1856 code: `
1857 function MyComponent() {
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+12 -2
@@ -330,8 +330,18 @@ export default {
330 duplicateDependencies.add(key);
331 }
332 } else {
333 - // Unnecessary dependency. Do nothing.
334 - unnecessaryDependencies.add(key);
333 + if (isEffect && !key.endsWith('.current')) {
334 + // Effects may have extra "unnecessary" deps.
335 + // Such as resetting scroll when ID changes.
336 + // The exception is ref.current which is always wrong.
337 + // Consider them legit.
338 + if (suggestedDependencies.indexOf(key) === -1) {
339 + suggestedDependencies.push(key);
340 + }
341 + } else {
342 + // Unnecessary dependency. Remember to report it.
343 + unnecessaryDependencies.add(key);
344 + }
345 }
346 });
347