Skip to content

Commit c2bb0a2

Browse files
authored
fix: fixed use-no-direct-set-state-in-use-effect deferred setState calls detection, closes #1117 (#1119)
1 parent a00a024 commit c2bb0a2

File tree

4 files changed

+73
-21
lines changed

4 files changed

+73
-21
lines changed

packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@ import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
33
import type { Scope } from "@typescript-eslint/utils/ts-eslint";
44
import * as AST from "@eslint-react/ast";
55
import * as ER from "@eslint-react/core";
6-
import { constVoid, getOrElseUpdate } from "@eslint-react/eff";
6+
import { constVoid, getOrElseUpdate, not } from "@eslint-react/eff";
77
import { getSettingsFromContext } from "@eslint-react/shared";
88
import * as VAR from "@eslint-react/var";
99
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
1010
import { match } from "ts-pattern";
1111

12-
import {
13-
isFromUseStateCall,
14-
isFunctionOfImmediatelyInvoked,
15-
isSetFunctionCall,
16-
isThenCall,
17-
isVariableDeclaratorFromHookCall,
18-
} from "../utils";
12+
import { isFromUseStateCall, isSetFunctionCall, isThenCall, isVariableDeclaratorFromHookCall } from "../utils";
1913

2014
type CallKind =
2115
| "useEffect"
@@ -98,10 +92,21 @@ export function useNoDirectSetStateInUseEffect<Ctx extends RuleContext>(
9892
}
9993

10094
function getFunctionKind(node: AST.TSESTreeFunction) {
101-
return match<AST.TSESTreeFunction, FunctionKind>(node)
102-
.when(isFunctionOfUseEffectSetup, () => "setup")
103-
.when(isFunctionOfImmediatelyInvoked, () => "immediate")
104-
.otherwise(() => "other");
95+
const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent;
96+
switch (true) {
97+
case node.async:
98+
case parent.type === T.CallExpression
99+
&& isThenCall(parent):
100+
return "deferred";
101+
case node.type !== T.FunctionDeclaration
102+
&& parent.type === T.CallExpression
103+
&& parent.callee === node:
104+
return "immediate";
105+
case isFunctionOfUseEffectSetup(node):
106+
return "setup";
107+
default:
108+
return "other";
109+
}
105110
}
106111

107112
return {
@@ -128,6 +133,11 @@ export function useNoDirectSetStateInUseEffect<Ctx extends RuleContext>(
128133
match(getCallKind(node))
129134
.with("setState", () => {
130135
switch (true) {
136+
case pEntry.kind === "deferred":
137+
case pEntry.node.async: {
138+
// do nothing, this is a deferred setState call
139+
break;
140+
}
131141
case pEntry.node === setupFunction:
132142
case pEntry.kind === "immediate"
133143
&& AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction: {

packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,57 @@ ruleTester.run(RULE_NAME, rule, {
695695
},
696696
],
697697
},
698+
// https://github.com/Rel1cx/eslint-react/issues/1117
699+
{
700+
code: tsx`
701+
import { useEffect, useState } from "react";
702+
703+
const Component1 = () => {
704+
const [foo, setFoo] = useState(null);
705+
const test = useCallback(() => {
706+
setFoo('') // warning (fine)
707+
fetch().then(() => { setFoo('') }); // warning (problem)
708+
}, [])
709+
useEffect(() => {
710+
test();
711+
fetch().then(() => { setFoo('') }); // no warning (fine)
712+
}, [test]);
713+
}
714+
`,
715+
errors: [
716+
{
717+
messageId: "noDirectSetStateInUseEffect",
718+
data: {
719+
name: "setFoo",
720+
},
721+
},
722+
],
723+
},
724+
{
725+
code: tsx`
726+
import { useEffect, useState } from "react";
727+
728+
const Component1 = () => {
729+
const [foo, setFoo] = useState(null);
730+
const test = () => {
731+
setFoo('') // warning (fine)
732+
fetch().then(() => { setFoo('') }); // no warning (fine)
733+
}
734+
useEffect(() => {
735+
test();
736+
fetch().then(() => { setFoo('') }); // no warning (fine)
737+
}, [test]);
738+
}
739+
`,
740+
errors: [
741+
{
742+
messageId: "noDirectSetStateInUseEffect",
743+
data: {
744+
name: "setFoo",
745+
},
746+
},
747+
],
748+
},
698749
{
699750
code: tsx`
700751
import { useEffect, useState } from "react";

packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export * from "./create-rule";
22
export * from "./is-from-hook-call";
33
export * from "./is-from-use-state-call";
4-
export * from "./is-function-of-immediately-invoked";
54
export * from "./is-react-hook-identifier";
65
export * from "./is-set-function-call";
76
export * from "./is-then-call";

packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)