Warn about setState directly in dep-less useEffect (#15184)
Dan Abramov committed
Mar 22, 2019 at 13:41 UTC
b1cccd1ed14ff0d5db4fcefbf8b49807a48d7410
2 files changed
+200
-37
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+108
@@ -992,6 +992,18 @@ const tests = {
992
}
993
`,
994
},
995
+ {
996
+ code: `
997
+ function Hello() {
998
+ const [state, setState] = useState(0);
999
+ useEffect(() => {
1000
+ const handleResize = () => setState(window.innerWidth);
1001
+ window.addEventListener('resize', handleResize);
1002
+ return () => window.removeEventListener('resize', handleResize);
1003
+ });
1004
+ }
1005
+ `,
1006
+ },
1007
],
1008
invalid: [
1009
{
@@ -4462,6 +4474,102 @@ const tests = {
4474
`Either exclude it or remove the dependency array.`,
4475
],
4476
},
4477
+ {
4478
+ code: `
4479
+ function Hello() {
4480
+ const [state, setState] = useState(0);
4481
+ useEffect(() => {
4482
+ setState({});
4483
+ });
4484
+ }
4485
+ `,
4486
+ output: `
4487
+ function Hello() {
4488
+ const [state, setState] = useState(0);
4489
+ useEffect(() => {
4490
+ setState({});
4491
+ }, []);
4492
+ }
4493
+ `,
4494
+ errors: [
4495
+ `React Hook useEffect contains a call to 'setState'. ` +
4496
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
4497
+ `To fix this, pass [] as a second argument to the useEffect Hook.`,
4498
+ ],
4499
+ },
4500
+ {
4501
+ code: `
4502
+ function Hello() {
4503
+ const [data, setData] = useState(0);
4504
+ useEffect(() => {
4505
+ fetchData.then(setData);
4506
+ });
4507
+ }
4508
+ `,
4509
+ output: `
4510
+ function Hello() {
4511
+ const [data, setData] = useState(0);
4512
+ useEffect(() => {
4513
+ fetchData.then(setData);
4514
+ }, []);
4515
+ }
4516
+ `,
4517
+ errors: [
4518
+ `React Hook useEffect contains a call to 'setData'. ` +
4519
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
4520
+ `To fix this, pass [] as a second argument to the useEffect Hook.`,
4521
+ ],
4522
+ },
4523
+ {
4524
+ code: `
4525
+ function Hello({ country }) {
4526
+ const [data, setData] = useState(0);
4527
+ useEffect(() => {
4528
+ fetchData(country).then(setData);
4529
+ });
4530
+ }
4531
+ `,
4532
+ output: `
4533
+ function Hello({ country }) {
4534
+ const [data, setData] = useState(0);
4535
+ useEffect(() => {
4536
+ fetchData(country).then(setData);
4537
+ }, [country]);
4538
+ }
4539
+ `,
4540
+ errors: [
4541
+ `React Hook useEffect contains a call to 'setData'. ` +
4542
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
4543
+ `To fix this, pass [country] as a second argument to the useEffect Hook.`,
4544
+ ],
4545
+ },
4546
+ {
4547
+ code: `
4548
+ function Hello({ prop1, prop2 }) {
4549
+ const [state, setState] = useState(0);
4550
+ useEffect(() => {
4551
+ if (prop1) {
4552
+ setState(prop2);
4553
+ }
4554
+ });
4555
+ }
4556
+ `,
4557
+ output: `
4558
+ function Hello({ prop1, prop2 }) {
4559
+ const [state, setState] = useState(0);
4560
+ useEffect(() => {
4561
+ if (prop1) {
4562
+ setState(prop2);
4563
+ }
4564
+ }, [prop1, prop2]);
4565
+ }
4566
+ `,
4567
+ errors: [
4568
+ `React Hook useEffect contains a call to 'setState'. ` +
4569
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
4570
+ `To fix this, pass [prop1, prop2] as a second argument to the useEffect Hook.`,
4571
+ ],
4572
+ },
4573
{
4574
code: `
4575
function Thing() {
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+92
-37
@@ -468,9 +468,101 @@ export default {
468
},
469
);
470
471
+ // Warn about assigning to variables in the outer scope.
472
+ // Those are usually bugs.
473
+ let staleAssignments = new Set();
474
+ function reportStaleAssignment(writeExpr, key) {
475
+ if (staleAssignments.has(key)) {
476
+ return;
477
+ }
478
+ staleAssignments.add(key);
479
+ context.report({
480
+ node: writeExpr,
481
+ message:
482
+ `Assignments to the '${key}' variable from inside React Hook ` +
483
+ `${context.getSource(reactiveHook)} will be lost after each ` +
484
+ `render. To preserve the value over time, store it in a useRef ` +
485
+ `Hook and keep the mutable value in the '.current' property. ` +
486
+ `Otherwise, you can move this variable directly inside ` +
487
+ `${context.getSource(reactiveHook)}.`,
488
+ });
489
+ }
490
+
491
+ // Remember which deps are optional and report bad usage first.
492
+ const optionalDependencies = new Set();
493
+ dependencies.forEach(({isStatic, references}, key) => {
494
+ if (isStatic) {
495
+ optionalDependencies.add(key);
496
+ }
497
+ references.forEach(reference => {
498
+ if (reference.writeExpr) {
499
+ reportStaleAssignment(reference.writeExpr, key);
500
+ }
501
+ });
502
+ });
503
+
504
+ if (staleAssignments.size > 0) {
505
+ // The intent isn't clear so we'll wait until you fix those first.
506
+ return;
507
+ }
508
+
509
if (!declaredDependenciesNode) {
510
+ // Check if there are any top-level setState() calls.
511
+ // Those tend to lead to infinite loops.
512
+ let setStateInsideEffectWithoutDeps = null;
513
+ dependencies.forEach(({isStatic, references}, key) => {
514
+ if (setStateInsideEffectWithoutDeps) {
515
+ return;
516
+ }
517
+ references.forEach(reference => {
518
+ if (setStateInsideEffectWithoutDeps) {
519
+ return;
520
+ }
521
+
522
+ const id = reference.identifier;
523
+ const isSetState = setStateCallSites.has(id);
524
+ if (!isSetState) {
525
+ return;
526
+ }
527
+
528
+ let fnScope = reference.from;
529
+ while (fnScope.type !== 'function') {
530
+ fnScope = fnScope.upper;
531
+ }
532
+ const isDirectlyInsideEffect = fnScope.block === node;
533
+ if (isDirectlyInsideEffect) {
534
+ // TODO: we could potentially ignore early returns.
535
+ setStateInsideEffectWithoutDeps = key;
536
+ }
537
+ });
538
+ });
539
+ if (setStateInsideEffectWithoutDeps) {
540
+ let {suggestedDependencies} = collectRecommendations({
541
+ dependencies,
542
+ declaredDependencies: [],
543
+ optionalDependencies,
544
+ externalDependencies: new Set(),
545
+ isEffect: true,
546
+ });
547
+ context.report({
548
+ node: node.parent.callee,
549
+ message:
550
+ `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` +
551
+ `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
552
+ `To fix this, pass [` +
553
+ suggestedDependencies.join(', ') +
554
+ `] as a second argument to the ${reactiveHookName} Hook.`,
555
+ fix(fixer) {
556
+ return fixer.insertTextAfter(
557
+ node,
558
+ `, [${suggestedDependencies.join(', ')}]`,
559
+ );
560
+ },
561
+ });
562
+ }
563
return;
564
}
565
+
566
const declaredDependencies = [];
567
const externalDependencies = new Set();
568
if (declaredDependenciesNode.type !== 'ArrayExpression') {
@@ -569,43 +661,6 @@ export default {
661
});
662
}
663
572
- // Warn about assigning to variables in the outer scope.
573
- // Those are usually bugs.
574
- let staleAssignments = new Set();
575
- function reportStaleAssignment(writeExpr, key) {
576
- if (staleAssignments.has(key)) {
577
- return;
578
- }
579
- staleAssignments.add(key);
580
- context.report({
581
- node: writeExpr,
582
- message:
583
- `Assignments to the '${key}' variable from inside React Hook ` +
584
- `${context.getSource(reactiveHook)} will be lost after each ` +
585
- `render. To preserve the value over time, store it in a useRef ` +
586
- `Hook and keep the mutable value in the '.current' property. ` +
587
- `Otherwise, you can move this variable directly inside ` +
588
- `${context.getSource(reactiveHook)}.`,
589
- });
590
- }
591
-
592
- // Remember which deps are optional and report bad usage first.
593
- const optionalDependencies = new Set();
594
- dependencies.forEach(({isStatic, references}, key) => {
595
- if (isStatic) {
596
- optionalDependencies.add(key);
597
- }
598
- references.forEach(reference => {
599
- if (reference.writeExpr) {
600
- reportStaleAssignment(reference.writeExpr, key);
601
- }
602
- });
603
- });
604
- if (staleAssignments.size > 0) {
605
- // The intent isn't clear so we'll wait until you fix those first.
606
- return;
607
- }
608
-
664
let {
665
suggestedDependencies,
666
unnecessaryDependencies,