Warn about dependencies outside of render scope (#14990)
Dan Abramov committed
Mar 1, 2019 at 18:16 UTC
59ef28437ac30ae26fe53a3be1de50754be8b806
2 files changed
+189
-15
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+160
-14
@@ -592,15 +592,6 @@ const tests = {
592
}
593
`,
594
},
595
- {
596
- // It is valid for effects to over-specify their deps.
597
- // TODO: maybe only allow local ones, and disallow this example.
598
- code: `
599
- function MyComponent() {
600
- useEffect(() => {}, [window]);
601
- }
602
- `,
603
- },
595
{
596
// It is valid for effects to over-specify their deps.
597
code: `
@@ -897,8 +888,6 @@ const tests = {
888
],
889
},
890
{
900
- // TODO: this case is weird.
901
- // Maybe it should not consider local1 unused despite component nesting?
891
code: `
892
function MyComponent() {
893
const local1 = {};
@@ -924,9 +913,10 @@ const tests = {
913
}
914
`,
915
errors: [
927
- // Focus on the more important part first (missing dep)
916
"React Hook useCallback has a missing dependency: 'local2'. " +
929
- 'Either include it or remove the dependency array.',
917
+ 'Either include it or remove the dependency array. ' +
918
+ "Values like 'local1' aren't valid dependencies " +
919
+ "because their mutation doesn't re-render the component.",
920
],
921
},
922
{
@@ -990,7 +980,9 @@ const tests = {
980
`,
981
errors: [
982
"React Hook useCallback has an unnecessary dependency: 'window'. " +
993
- 'Either exclude it or remove the dependency array.',
983
+ 'Either exclude it or remove the dependency array. ' +
984
+ "Values like 'window' aren't valid dependencies " +
985
+ "because their mutation doesn't re-render the component.",
986
],
987
},
988
{
@@ -2530,6 +2522,160 @@ const tests = {
2522
'Either include it or remove the dependency array.',
2523
],
2524
},
2525
+ {
2526
+ code: `
2527
+ function MyComponent() {
2528
+ useEffect(() => {
2529
+ window.scrollTo(0, 0);
2530
+ }, [window]);
2531
+ }
2532
+ `,
2533
+ errors: [
2534
+ "React Hook useEffect has an unnecessary dependency: 'window'. " +
2535
+ 'Either exclude it or remove the dependency array. ' +
2536
+ "Values like 'window' aren't valid dependencies " +
2537
+ "because their mutation doesn't re-render the component.",
2538
+ ],
2539
+ },
2540
+ {
2541
+ code: `
2542
+ import MutableStore from 'store';
2543
+
2544
+ function MyComponent() {
2545
+ useEffect(() => {
2546
+ console.log(MutableStore.hello);
2547
+ }, [MutableStore.hello]);
2548
+ }
2549
+ `,
2550
+ output: `
2551
+ import MutableStore from 'store';
2552
+
2553
+ function MyComponent() {
2554
+ useEffect(() => {
2555
+ console.log(MutableStore.hello);
2556
+ }, []);
2557
+ }
2558
+ `,
2559
+ errors: [
2560
+ "React Hook useEffect has an unnecessary dependency: 'MutableStore.hello'. " +
2561
+ 'Either exclude it or remove the dependency array. ' +
2562
+ "Values like 'MutableStore.hello' aren't valid dependencies " +
2563
+ "because their mutation doesn't re-render the component.",
2564
+ ],
2565
+ },
2566
+ {
2567
+ code: `
2568
+ import MutableStore from 'store';
2569
+ let z = {};
2570
+
2571
+ function MyComponent(props) {
2572
+ let x = props.foo;
2573
+ {
2574
+ let y = props.bar;
2575
+ useEffect(() => {
2576
+ console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff);
2577
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
2578
+ }
2579
+ }
2580
+ `,
2581
+ output: `
2582
+ import MutableStore from 'store';
2583
+ let z = {};
2584
+
2585
+ function MyComponent(props) {
2586
+ let x = props.foo;
2587
+ {
2588
+ let y = props.bar;
2589
+ useEffect(() => {
2590
+ console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff);
2591
+ }, [props.foo, x, y]);
2592
+ }
2593
+ }
2594
+ `,
2595
+ errors: [
2596
+ 'React Hook useEffect has unnecessary dependencies: ' +
2597
+ "'MutableStore.hello.world', 'global.stuff', and 'z'. " +
2598
+ 'Either exclude them or remove the dependency array. ' +
2599
+ "Values like 'MutableStore.hello.world' aren't valid dependencies " +
2600
+ "because their mutation doesn't re-render the component.",
2601
+ ],
2602
+ },
2603
+ {
2604
+ code: `
2605
+ import MutableStore from 'store';
2606
+ let z = {};
2607
+
2608
+ function MyComponent(props) {
2609
+ let x = props.foo;
2610
+ {
2611
+ let y = props.bar;
2612
+ useEffect(() => {
2613
+ // nothing
2614
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
2615
+ }
2616
+ }
2617
+ `,
2618
+ // The output should contain the ones that are inside a component
2619
+ // since there are legit reasons to over-specify them for effects.
2620
+ output: `
2621
+ import MutableStore from 'store';
2622
+ let z = {};
2623
+
2624
+ function MyComponent(props) {
2625
+ let x = props.foo;
2626
+ {
2627
+ let y = props.bar;
2628
+ useEffect(() => {
2629
+ // nothing
2630
+ }, [props.foo, x, y]);
2631
+ }
2632
+ }
2633
+ `,
2634
+ errors: [
2635
+ 'React Hook useEffect has unnecessary dependencies: ' +
2636
+ "'MutableStore.hello.world', 'global.stuff', and 'z'. " +
2637
+ 'Either exclude them or remove the dependency array. ' +
2638
+ "Values like 'MutableStore.hello.world' aren't valid dependencies " +
2639
+ "because their mutation doesn't re-render the component.",
2640
+ ],
2641
+ },
2642
+ {
2643
+ code: `
2644
+ import MutableStore from 'store';
2645
+ let z = {};
2646
+
2647
+ function MyComponent(props) {
2648
+ let x = props.foo;
2649
+ {
2650
+ let y = props.bar;
2651
+ const fn = useCallback(() => {
2652
+ // nothing
2653
+ }, [MutableStore.hello.world, props.foo, x, y, z, global.stuff]);
2654
+ }
2655
+ }
2656
+ `,
2657
+ output: `
2658
+ import MutableStore from 'store';
2659
+ let z = {};
2660
+
2661
+ function MyComponent(props) {
2662
+ let x = props.foo;
2663
+ {
2664
+ let y = props.bar;
2665
+ const fn = useCallback(() => {
2666
+ // nothing
2667
+ }, []);
2668
+ }
2669
+ }
2670
+ `,
2671
+ errors: [
2672
+ 'React Hook useCallback has unnecessary dependencies: ' +
2673
+ "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " +
2674
+ 'Either exclude them or remove the dependency array. ' +
2675
+ "Values like 'MutableStore.hello.world' aren't valid dependencies " +
2676
+ "because their mutation doesn't re-render the component.",
2677
+ ],
2678
+ },
2679
],
2680
};
2681
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+29
-1
@@ -86,6 +86,7 @@ export default {
86
// scope. We can't enforce this in a lint so we trust that all variables
87
// declared outside of pure scope are indeed frozen.
88
const pureScopes = new Set();
89
+ let componentScope = null;
90
{
91
let currentScope = scope.upper;
92
while (currentScope) {
@@ -101,6 +102,7 @@ export default {
102
if (!currentScope) {
103
return;
104
}
105
+ componentScope = currentScope;
106
}
107
108
// These are usually mistaken. Collect them.
@@ -224,6 +226,7 @@ export default {
226
);
227
228
const declaredDependencies = [];
229
+ const externalDependencies = new Set();
230
if (declaredDependenciesNode.type !== 'ArrayExpression') {
231
// If the declared dependencies are not an array expression then we
232
// can't verify that the user provided the correct dependencies. Tell
@@ -298,11 +301,24 @@ export default {
301
throw error;
302
}
303
}
304
+
305
+ let maybeID = declaredDependencyNode;
306
+ while (maybeID.type === 'MemberExpression') {
307
+ maybeID = maybeID.object;
308
+ }
309
+ const isDeclaredInComponent = !componentScope.through.some(
310
+ ref => ref.identifier === maybeID,
311
+ );
312
+
313
// Add the dependency to our declared dependency map.
314
declaredDependencies.push({
315
key: declaredDependency,
316
node: declaredDependencyNode,
317
});
318
+
319
+ if (!isDeclaredInComponent) {
320
+ externalDependencies.add(declaredDependency);
321
+ }
322
});
323
}
324
@@ -348,6 +364,7 @@ export default {
364
dependencies,
365
declaredDependencies,
366
optionalDependencies,
367
+ externalDependencies,
368
isEffect,
369
});
370
@@ -370,6 +387,7 @@ export default {
387
dependencies,
388
declaredDependencies: [], // Pretend we don't know
389
optionalDependencies,
390
+ externalDependencies,
391
isEffect,
392
}).suggestedDependencies;
393
}
@@ -423,6 +441,11 @@ export default {
441
extraWarning =
442
` Mutable values like '${badRef}' aren't valid dependencies ` +
443
"because their mutation doesn't re-render the component.";
444
+ } else if (externalDependencies.size > 0) {
445
+ const dep = Array.from(externalDependencies)[0];
446
+ extraWarning =
447
+ ` Values like '${dep}' aren't valid dependencies ` +
448
+ `because their mutation doesn't re-render the component.`;
449
}
450
}
451
@@ -462,6 +485,7 @@ function collectRecommendations({
485
dependencies,
486
declaredDependencies,
487
optionalDependencies,
488
+ externalDependencies,
489
isEffect,
490
}) {
491
// Our primary data structure.
@@ -584,7 +608,11 @@ function collectRecommendations({
608
duplicateDependencies.add(key);
609
}
610
} else {
587
- if (isEffect && !key.endsWith('.current')) {
611
+ if (
612
+ isEffect &&
613
+ !key.endsWith('.current') &&
614
+ !externalDependencies.has(key)
615
+ ) {
616
// Effects are allowed extra "unnecessary" deps.
617
// Such as resetting scroll when ID changes.
618
// Consider them legit.