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