diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts index 6ac19cafb..84d2fafb7 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts @@ -3,19 +3,13 @@ import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; import type { Scope } from "@typescript-eslint/utils/ts-eslint"; import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; -import { constVoid, getOrElseUpdate } from "@eslint-react/eff"; +import { constVoid, getOrElseUpdate, not } from "@eslint-react/eff"; import { getSettingsFromContext } from "@eslint-react/shared"; import * as VAR from "@eslint-react/var"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { match } from "ts-pattern"; -import { - isFromUseStateCall, - isFunctionOfImmediatelyInvoked, - isSetFunctionCall, - isThenCall, - isVariableDeclaratorFromHookCall, -} from "../utils"; +import { isFromUseStateCall, isSetFunctionCall, isThenCall, isVariableDeclaratorFromHookCall } from "../utils"; type CallKind = | "useEffect" @@ -98,10 +92,21 @@ export function useNoDirectSetStateInUseEffect( } function getFunctionKind(node: AST.TSESTreeFunction) { - return match(node) - .when(isFunctionOfUseEffectSetup, () => "setup") - .when(isFunctionOfImmediatelyInvoked, () => "immediate") - .otherwise(() => "other"); + const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent; + switch (true) { + case node.async: + case parent.type === T.CallExpression + && isThenCall(parent): + return "deferred"; + case node.type !== T.FunctionDeclaration + && parent.type === T.CallExpression + && parent.callee === node: + return "immediate"; + case isFunctionOfUseEffectSetup(node): + return "setup"; + default: + return "other"; + } } return { @@ -128,6 +133,11 @@ export function useNoDirectSetStateInUseEffect( match(getCallKind(node)) .with("setState", () => { switch (true) { + case pEntry.kind === "deferred": + case pEntry.node.async: { + // do nothing, this is a deferred setState call + break; + } case pEntry.node === setupFunction: case pEntry.kind === "immediate" && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction: { diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.spec.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.spec.ts index bc562aae7..f36ef9ac1 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.spec.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.spec.ts @@ -695,6 +695,57 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + // https://github.com/Rel1cx/eslint-react/issues/1117 + { + code: tsx` + import { useEffect, useState } from "react"; + + const Component1 = () => { + const [foo, setFoo] = useState(null); + const test = useCallback(() => { + setFoo('') // warning (fine) + fetch().then(() => { setFoo('') }); // warning (problem) + }, []) + useEffect(() => { + test(); + fetch().then(() => { setFoo('') }); // no warning (fine) + }, [test]); + } + `, + errors: [ + { + messageId: "noDirectSetStateInUseEffect", + data: { + name: "setFoo", + }, + }, + ], + }, + { + code: tsx` + import { useEffect, useState } from "react"; + + const Component1 = () => { + const [foo, setFoo] = useState(null); + const test = () => { + setFoo('') // warning (fine) + fetch().then(() => { setFoo('') }); // no warning (fine) + } + useEffect(() => { + test(); + fetch().then(() => { setFoo('') }); // no warning (fine) + }, [test]); + } + `, + errors: [ + { + messageId: "noDirectSetStateInUseEffect", + data: { + name: "setFoo", + }, + }, + ], + }, { code: tsx` import { useEffect, useState } from "react"; diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts index 416b9a5d6..8807643c6 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/index.ts @@ -1,7 +1,6 @@ export * from "./create-rule"; export * from "./is-from-hook-call"; export * from "./is-from-use-state-call"; -export * from "./is-function-of-immediately-invoked"; export * from "./is-react-hook-identifier"; export * from "./is-set-function-call"; export * from "./is-then-call"; diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts deleted file mode 100644 index 3ba5e9e58..000000000 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/utils/is-function-of-immediately-invoked.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type * as AST from "@eslint-react/ast"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -export function isFunctionOfImmediatelyInvoked(node: AST.TSESTreeFunction): boolean { - return node.type !== T.FunctionDeclaration - && node.parent.type === T.CallExpression - && node.parent.callee === node; -}