diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts
index 217288fc3b..59ff1f32f8 100644
--- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts
@@ -30,7 +30,6 @@ export const rules = {
// "react-x/prefer-destructuring-assignment": "warn",
// "react-x/prefer-namespace-import": "warn",
// "react-x/prefer-read-only-props": "error",
- "react-x/no-direct-set-state-in-use-effect": "warn",
"react-x/no-duplicate-key": "warn",
"react-x/no-forward-ref": "warn",
"react-x/no-implicit-key": "warn",
diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts
index 144bf74671..d0b79b1db3 100644
--- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts
@@ -25,8 +25,6 @@ import noContextProvider from "./rules/no-context-provider";
import noCreateRef from "./rules/no-create-ref";
import noDefaultProps from "./rules/no-default-props";
import noDirectMutationState from "./rules/no-direct-mutation-state";
-import noDirectSetStateInUseEffect from "./rules/no-direct-set-state-in-use-effect";
-import noDirectSetStateInUseLayoutEffect from "./rules/no-direct-set-state-in-use-layout-effect";
import noDuplicateKey from "./rules/no-duplicate-key";
import noForwardRef from "./rules/no-forward-ref";
import noImplicitKey from "./rules/no-implicit-key";
@@ -44,6 +42,7 @@ import noSetStateInComponentDidUpdate from "./rules/no-set-state-in-component-di
import noSetStateInComponentWillUpdate from "./rules/no-set-state-in-component-will-update";
import noStringRefs from "./rules/no-string-refs";
import noUnnecessaryUseCallback from "./rules/no-unnecessary-use-callback";
+import noUnnecessaryUseEffect from "./rules/no-unnecessary-use-effect";
import noUnnecessaryUseMemo from "./rules/no-unnecessary-use-memo";
import noUnnecessaryUsePrefix from "./rules/no-unnecessary-use-prefix";
import noUnsafeComponentWillMount from "./rules/no-unsafe-component-will-mount";
@@ -69,6 +68,7 @@ export const plugin = {
"jsx-key-before-spread": jsxKeyBeforeSpread,
"jsx-no-comment-textnodes": jsxNoCommentTextnodes,
"jsx-no-duplicate-props": jsxNoDuplicateProps,
+ "jsx-no-iife": jsxNoIife,
"jsx-no-undef": jsxNoUndef,
"jsx-shorthand-boolean": jsxShorthandBoolean,
"jsx-shorthand-fragment": jsxShorthandFragment,
@@ -91,8 +91,6 @@ export const plugin = {
"no-create-ref": noCreateRef,
"no-default-props": noDefaultProps,
"no-direct-mutation-state": noDirectMutationState,
- "no-direct-set-state-in-use-effect": noDirectSetStateInUseEffect,
- "no-direct-set-state-in-use-layout-effect": noDirectSetStateInUseLayoutEffect,
"no-duplicate-key": noDuplicateKey,
"no-forward-ref": noForwardRef,
"no-implicit-key": noImplicitKey,
@@ -110,6 +108,7 @@ export const plugin = {
"no-set-state-in-component-will-update": noSetStateInComponentWillUpdate,
"no-string-refs": noStringRefs,
"no-unnecessary-use-callback": noUnnecessaryUseCallback,
+ "no-unnecessary-use-effect": noUnnecessaryUseEffect,
"no-unnecessary-use-memo": noUnnecessaryUseMemo,
"no-unnecessary-use-prefix": noUnnecessaryUsePrefix,
"no-unsafe-component-will-mount": noUnsafeComponentWillMount,
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.md
deleted file mode 100644
index cdbac64ddb..0000000000
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.md
+++ /dev/null
@@ -1,314 +0,0 @@
----
-title: no-direct-set-state-in-use-layout-effect
----
-
-**Full Name in `eslint-plugin-react-x`**
-
-```sh copy
-react-x/no-direct-set-state-in-use-layout-effect
-```
-
-**Full Name in `@eslint-react/eslint-plugin`**
-
-```sh copy
-@eslint-react/no-direct-set-state-in-use-layout-effect
-```
-
-**Features**
-
-`🧪`
-
-**Presets**
-
-- `recommended`
-- `recommended-typescript`
-- `recommended-type-checked`
-
-## Description
-
-Disallow **direct** calls to the [`set` function](https://react.dev/reference/react/useState#setstate) of `useState` in `useLayoutEffect`.
-
-Directly setting state in `useLayoutEffect` can lead to:
-
-- **Redundant state**: You might be duplicating derived values that could be computed during render.
-- **Unnecessary effects**: Triggering re-renders that could be avoided.
-- **Confusing logic**: It can make component behavior harder to reason about.
-
-### What counts as a violation?
-
-This is **not allowed**:
-
-```tsx
-useLayoutEffect(() => {
- setFullName(firstName + " " + lastName);
-}, [firstName, lastName]);
-```
-
-Instead, compute the value during render:
-
-```tsx
-const fullName = firstName + " " + lastName;
-```
-
-### What is allowed?
-
-The rule **does not flag** indirect calls, such as:
-
-- Inside event handlers.
-- Inside `async` functions.
-- Inside `setTimeout`, `setInterval`, `Promise.then`, etc.
-
-### Known limitations
-
-- It doesn’t check `set` calls in `useLayoutEffect` cleanup functions.
-
- ```tsx {2}
- useLayoutEffect(() => {
- return () => {
- setFullName(firstName + " " + lastName); // ❌ Direct call
- };
- }, [firstName, lastName]);
- ```
-
-- It doesn’t detect `set` calls in `async` functions are being called before or after the `await` statement.
-
- ```tsx {2}
- useLayoutEffect(() => {
- const fetchData = async () => {
- setFullName(data.name); // ❌ Direct call
- };
- fetchData();
- }, []);
- ```
-
-## Examples
-
-The first three cases are common valid use cases because they are not called the `set` function directly in `useLayoutEffect`:
-
-### Passing
-
-```tsx
-import { useState, useLayoutEffect } from "react";
-
-export default function Counter() {
- const [count, setCount] = useState(0);
-
- useLayoutEffect(() => {
- const handler = () => setCount((c) => c + 1);
- window.addEventListener("click", handler);
- return () => window.removeEventListener("click", handler);
- }, []);
-
- return
{count}
;
-}
-```
-
-### Passing
-
-```tsx
-import { useState, useLayoutEffect } from "react";
-
-export default function Counter() {
- const [count, setCount] = useState(0);
-
- useLayoutEffect(() => {
- const intervalId = setInterval(() => {
- setCount((c) => c + 1);
- }, 1000);
- return () => clearInterval(intervalId);
- }, []);
-
- return {count}
;
-}
-```
-
-### Passing
-
-```tsx
-import { useState, useLayoutEffect } from "react";
-
-export default function RemoteContent() {
- const [content, setContent] = useState("");
-
- useLayoutEffect(() => {
- let discarded = false;
- fetch("https://eslint-react.xyz/content")
- .then((resp) => resp.text())
- .then((text) => {
- if (discarded) return;
- setContent(text);
- });
- return () => {
- discarded = true;
- };
- }, []);
-
- return {count}
;
-}
-```
-
-The following examples are derived from the [React documentation](https://react.dev/learn/you-might-not-need-an-effect):
-
-### Failing
-
-```tsx
-import { useLayoutEffect, useState } from "react";
-
-function Form() {
- const [firstName, setFirstName] = useState("Taylor");
- const [lastName, setLastName] = useState("Swift");
-
- // 🔴 Avoid: redundant state and unnecessary Effect
- const [fullName, setFullName] = useState("");
- useLayoutEffect(() => {
- setFullName(firstName + " " + lastName);
- }, [firstName, lastName]);
- // ...
-}
-```
-
-### Passing
-
-```tsx
-import { useState } from "react";
-
-function Form() {
- const [firstName, setFirstName] = useState("Taylor");
- const [lastName, setLastName] = useState("Swift");
- // ✅ Good: calculated during rendering
- const fullName = firstName + " " + lastName;
- // ...
-}
-```
-
-### Failing
-
-```tsx
-import { useLayoutEffect, useState } from "react";
-
-function TodoList({ todos, filter }) {
- const [newTodo, setNewTodo] = useState("");
-
- // 🔴 Avoid: redundant state and unnecessary Effect
- const [visibleTodos, setVisibleTodos] = useState([]);
- useLayoutEffect(() => {
- setVisibleTodos(getFilteredTodos(todos, filter));
- }, [todos, filter]);
-
- // ...
-}
-```
-
-### Passing
-
-```tsx
-import { useMemo, useState } from "react";
-
-function TodoList({ todos, filter }) {
- const [newTodo, setNewTodo] = useState("");
- // ✅ Does not re-run getFilteredTodos() unless todos or filter change
- const visibleTodos = useMemo(
- () => getFilteredTodos(todos, filter),
- [todos, filter],
- );
- // ...
-}
-```
-
-### Failing
-
-```tsx
-import { useLayoutEffect, useState } from "react";
-
-export default function ProfilePage({ userId }) {
- const [comment, setComment] = useState("");
-
- // 🔴 Avoid: Resetting state on prop change in an Effect
- useLayoutEffect(() => {
- setComment("");
- }, [userId]);
- // ...
-}
-```
-
-### Passing
-
-```tsx
-import { useState } from "react";
-
-export default function ProfilePage({ userId }) {
- return ;
-}
-
-function Profile({ userId }) {
- // ✅ This and any other state below will reset on key change automatically
- const [comment, setComment] = useState("");
- // ...
-}
-```
-
-### Failing
-
-```tsx
-import { useLayoutEffect, useState } from "react";
-
-function List({ items }) {
- const [isReverse, setIsReverse] = useState(false);
- const [selection, setSelection] = useState(null);
-
- // 🔴 Avoid: Adjusting state on prop change in an Effect
- useLayoutEffect(() => {
- setSelection(null);
- }, [items]);
- // ...
-}
-```
-
-### Passing
-
-```tsx
-import { useState } from "react";
-
-function List({ items }) {
- const [isReverse, setIsReverse] = useState(false);
- const [selection, setSelection] = useState(null);
-
- // Better: Adjust the state while rendering
- const [prevItems, setPrevItems] = useState(items);
- if (items !== prevItems) {
- setPrevItems(items);
- setSelection(null);
- }
- // ...
-}
-```
-
-```tsx
-import { useState } from "react";
-
-function List({ items }) {
- const [isReverse, setIsReverse] = useState(false);
- const [selectedId, setSelectedId] = useState(null);
- // ✅ Best: Calculate everything during rendering
- const selection = items.find((item) => item.id === selectedId) ?? null;
- // ...
-}
-```
-
-## Implementation
-
-- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts)
-- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.spec.ts)
-
-## Further Reading
-
-- [React Docs: `useState` Hook](https://react.dev/reference/react/useState)
-- [React Docs: `useLayoutEffect` Hook](https://react.dev/reference/react/useLayoutEffect)
-- [React Docs: You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
-
----
-
-## See Also
-
-- [`no-direct-set-state-in-use-effect`](./no-direct-set-state-in-use-effect)\
- Disallow **direct** calls to the [`set` function](https://react.dev/reference/react/useState#setstate) of `useState` in `useEffect`.
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.spec.ts
deleted file mode 100644
index 7fc104535f..0000000000
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.spec.ts
+++ /dev/null
@@ -1,815 +0,0 @@
-import tsx from "dedent";
-
-import { allValid, ruleTester } from "../../../../../test";
-import rule, { RULE_NAME } from "./no-direct-set-state-in-use-layout-effect";
-
-ruleTester.run(RULE_NAME, rule, {
- invalid: [
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const [data, setData] = useState(0);
- useLayoutEffect(() => {
- setData(1);
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useState } from "react";
-
- function Component() {
- const [data, setData] = useState(0);
- useIsomorphicLayoutEffect(() => {
- setData(1);
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- settings: {
- "react-x": {
- additionalHooks: {
- useEffect: ["useIsomorphicLayoutEffect"],
- useLayoutEffect: ["useIsomorphicLayoutEffect"],
- },
- },
- },
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const data = useState(0);
- useLayoutEffect(() => {
- data[1]();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const data = useState(0);
- useLayoutEffect(() => {
- data.at(1)();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const index = 1;
- function Component() {
- const data = useState(0);
- useLayoutEffect(() => {
- data.at(index)();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const index = 1;
- function Component() {
- const data = useState(0);
- useLayoutEffect(() => {
- data[index]();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect } from "react";
-
- const index = 1;
- function Component() {
- const data = useCustomState(0);
- useLayoutEffect(() => {
- data[index]();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- settings: {
- "react-x": {
- additionalHooks: {
- useState: ["useCustomState"],
- },
- },
- },
- },
- {
- code: tsx`
- import { useState } from "react";
-
- const index = 1;
- function Component() {
- const data = useState(0);
- useCustomEffect(() => {
- data[index]();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- settings: {
- "react-x": {
- additionalHooks: {
- useLayoutEffect: ["useCustomEffect"],
- },
- },
- },
- },
- {
- code: tsx`
- const index = 1;
- function Component() {
- const data = useCustomState(0);
- useCustomEffect(() => {
- data[index]();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- settings: {
- "react-x": {
- additionalHooks: {
- useLayoutEffect: ["useCustomEffect"],
- useState: ["useCustomState"],
- },
- },
- },
- },
- {
- code: tsx`
- const index = 1;
- function Component() {
- const data = useCustomState(0);
- useCustomEffect(() => {
- data.at(index)();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- settings: {
- "react-x": {
- additionalHooks: {
- useLayoutEffect: ["useCustomEffect"],
- useState: ["useCustomState"],
- },
- },
- },
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const [data, setData] = useState(0);
- useLayoutEffect(() => {
- if (data === 0) {
- setData(1);
- }
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => {
- const onLoad = () => {
- setData();
- };
- onLoad();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data1, setData1] = useState();
- const [data2, setData2] = useState();
- const setAll = () => {
- setData1();
- setData2();
- }
- useLayoutEffect(() => {
- setAll();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect", data: { name: "setData1" } },
- { messageId: "noDirectSetStateInUseLayoutEffect", data: { name: "setData2" } },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState, useCallback } from "react";
-
- const Component = () => {
- const [data1, setData1] = useState();
- const [data2, setData2] = useState();
- const setAll = useCallback(() => {
- setData1();
- setData2();
- })
- useLayoutEffect(() => {
- setAll();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect", data: { name: "setData1" } },
- { messageId: "noDirectSetStateInUseLayoutEffect", data: { name: "setData2" } },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => {
- (() => { setData() })();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => {
- !(function onLoad() {
- setData()
- })();
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => {
- const setAll = () => {
- setData();
- }
- setAll()
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState, useCallback } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- const setAll = useCallback(() => setData(), []);
- useLayoutEffect(() => {
- setAll()
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState, useMemo } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- const setAll = useMemo(() => () => setData(), []);
- useLayoutEffect(() => {
- setAll()
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState, useCallback } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- const setAll = useCallback(setData, []);
- useLayoutEffect(() => {
- setAll()
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState, useMemo } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- const setAll = useMemo(() => setData, []);
- useLayoutEffect(() => {
- setAll()
- }, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState, useMemo } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- const setAll = useMemo(() => setData, []);
- useLayoutEffect(setAll, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- // TODO: Add cleanup function check
- // {
- // code: tsx`
- // import { useLayoutEffect, useState } from "react";
-
- // const Component = () => {
- // const [data, setData] = useState();
- // useLayoutEffect(() => {
- // return () => {
- // setData();
- // }
- // }, []);
- // return null;
- // }
- // `,
- // errors: [
- // { messageId: "noDirectSetStateInUseLayoutEffect" },
- // ],
- // },
- // TODO: Add cleanup function check
- // {
- // code: tsx`
- // import { useLayoutEffect, useState } from "react";
-
- // const Component = () => {
- // const [data, setData] = useState();
- // useLayoutEffect(() => {
- // const cleanup = () => {
- // setData();
- // }
- // return cleanup;
- // }, []);
- // return null;
- // }
- // `,
- // errors: [
- // { messageId: "noDirectSetStateInUseLayoutEffect" },
- // ],
- // },
- // TODO: Add cleanup function check
- // {
- // code: tsx`
- // import { useLayoutEffect, useState } from "react";
-
- // const Component = () => {
- // const [data, setData] = useState();
- // useLayoutEffect(() => setData, []);
- // return null;
- // }
- // `,
- // errors: [
- // { messageId: "noDirectSetStateInUseLayoutEffect" },
- // ],
- // },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => setData(), []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(setData, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- const setupFunction = () => {
- setData()
- }
- useLayoutEffect(setupFunction, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- function setupFunction() {
- setData()
- }
- useLayoutEffect(setupFunction, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(setupFunction, []);
- function setupFunction() {
- setData()
- }
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component1 = () => {
- const [data, setData] = useState();
- const setupFunction = () => {
- setData()
- }
- useLayoutEffect(setupFunction, []);
- return null;
- }
-
- const Component2 = () => {
- const [data, setData] = useState();
- const setupFunction = () => {
- setData()
- }
- useLayoutEffect(setupFunction, []);
- return null;
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- {
- code: tsx`
- import { useLayoutEffect, useState } from "react";
-
- function useCustomHook() {
- const [data, setData] = useState();
- const handlerWatcher = () => {
- setData()
- }
- useLayoutEffect(() => {
- const abortController = new AbortController()
- new MutationObserverWatcher(searchAvatarMetaSelector())
- .addListener('onChange', handlerWatcher)
- .startWatch(
- {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['src'],
- },
- abortController.signal,
- )
- handlerWatcher();
- return () => abortController.abort()
- }, [handlerWatcher])
- }
- `,
- errors: [
- { messageId: "noDirectSetStateInUseLayoutEffect" },
- ],
- },
- ],
- valid: [
- ...allValid,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const [fn] = useState(() => () => "Function");
- // ...
- useLayoutEffect(() => {
- fn();
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const [data, setData] = useState(0);
- useLayoutEffect(() => {
- const handler = () => setData(1);
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- function Component() {
- const [data, setData] = useState(0);
- useLayoutEffect(() => {
- fetch().then(() => setData());
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => {
- const onLoad = () => {
- setData();
- };
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const index = 0;
- function Component() {
- const data = useState(() => 0);
- useLayoutEffect(() => {
- data.at(index)();
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const index = 0;
- function Component() {
- const [data, setData] = useState(() => 0);
- useLayoutEffect(() => {
- void async function () {
- const ret = await fetch("https://eslint-react.xyz");
- setData(ret);
- }()
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data1, setData1] = useState();
- const [data2, setData2] = useState();
- const setAll = () => {
- setData1();
- setData2();
- }
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data1, setData1] = useState();
- const [data2, setData2] = useState();
- const setAll = () => {
- setData1();
- setData2();
- }
- const handler = () => {
- setAll();
- }
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component = () => {
- const [data1, setData1] = useState();
- const [data2, setData2] = useState();
- function handler() {
- setAll();
- }
- function setAll() {
- setData1();
- setData2();
- }
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component1 = () => {
- const [data, setData] = useState();
- const setupFunction = () => {
- setData()
- }
- return null;
- }
-
- const Component2 = () => {
- const [data, setData] = useState();
- useLayoutEffect(setupFunction, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- const Component1 = () => {
- const [data, setData] = useState();
- const setAll = () => {
- setData();
- }
- return null;
- }
-
- const Component2 = () => {
- const [data, setData] = useState();
- useLayoutEffect(() => {
- setAll();
- }, []);
- return null;
- }
- `,
- tsx`
- import { useLayoutEffect, useState } from "react";
-
- function useCustomHook() {
- const [data, setData] = useState();
- const handlerWatcher = () => {
- setData()
- }
- useLayoutEffect(() => {
- const abortController = new AbortController()
- new MutationObserverWatcher(searchAvatarMetaSelector())
- .addListener('onChange', handlerWatcher)
- .startWatch(
- {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: ['src'],
- },
- abortController.signal,
- )
- return () => abortController.abort()
- }, [handlerWatcher])
- }
- `,
- tsx`
- import { useEffect, useState, useRef } from "react";
-
- function Tooltip() {
- const ref = useRef(null);
- const [tooltipHeight, setTooltipHeight] = useState(0); // You don't know real height yet
-
- useEffect(() => {
- const { height } = ref.current.getBoundingClientRect();
- setTooltipHeight(height); // Re-render now that you know the real height
- }, []);
-
- // ...use tooltipHeight in the rendering logic below...
- }
- `,
- ],
-});
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts
deleted file mode 100644
index 52141413ff..0000000000
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { RuleContext, RuleFeature } from "@eslint-react/kit";
-import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
-import type { CamelCase } from "string-ts";
-
-import { createRule } from "../utils";
-import { useNoDirectSetStateInUseEffect } from "./no-direct-set-state-in-use-effect";
-
-export const RULE_NAME = "no-direct-set-state-in-use-layout-effect";
-
-export const RULE_FEATURES = [
- "EXP",
-] as const satisfies RuleFeature[];
-
-type MessageID = CamelCase;
-
-export default createRule<[], MessageID>({
- meta: {
- type: "problem",
- docs: {
- description: "Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`.",
- [Symbol.for("rule_features")]: RULE_FEATURES,
- },
- messages: {
- noDirectSetStateInUseLayoutEffect:
- "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useLayoutEffect'.",
- },
- schema: [],
- },
- name: RULE_NAME,
- create,
- defaultOptions: [],
-});
-
-export function create(context: RuleContext): RuleListener {
- if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
- return useNoDirectSetStateInUseEffect(context, {
- onViolation(ctx, node, data) {
- ctx.report({ messageId: "noDirectSetStateInUseLayoutEffect", node, data });
- },
- useEffectKind: "useLayoutEffect",
- });
-}
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.md
similarity index 89%
rename from packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.md
rename to packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.md
index 2628604b9c..00ec554682 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.md
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.md
@@ -1,17 +1,17 @@
---
-title: no-direct-set-state-in-use-effect
+title: no-unnecessary-use-effect
---
**Full Name in `eslint-plugin-react-x`**
```sh copy
-react-x/no-direct-set-state-in-use-effect
+react-x/no-unnecessary-use-effect
```
**Full Name in `@eslint-react/eslint-plugin`**
```sh copy
-@eslint-react/no-direct-set-state-in-use-effect
+@eslint-react/no-unnecessary-use-effect
```
**Features**
@@ -26,13 +26,7 @@ react-x/no-direct-set-state-in-use-effect
## Description
-Disallow **direct** calls to the [`set` function](https://react.dev/reference/react/useState#setstate) of `useState` in `useEffect`.
-
-Directly setting state in `useEffect` can lead to:
-
-- **Redundant state**: You might be duplicating derived values that could be computed during render.
-- **Unnecessary effects**: Triggering re-renders that could be avoided.
-- **Confusing logic**: It can make component behavior harder to reason about.
+Disallow unnecessary usage of `useEffect`.
### What counts as a violation?
@@ -297,8 +291,8 @@ function List({ items }) {
## Implementation
-- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts)
-- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.spec.ts)
+- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts)
+- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.spec.ts)
## Further Reading
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.spec.ts
similarity index 88%
rename from packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.spec.ts
rename to packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.spec.ts
index bc562aae7b..cf2643e13c 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.spec.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.spec.ts
@@ -1,7 +1,7 @@
import tsx from "dedent";
import { allValid, ruleTester } from "../../../../../test";
-import rule, { RULE_NAME } from "./no-direct-set-state-in-use-effect";
+import rule, { RULE_NAME } from "./no-unnecessary-use-effect";
ruleTester.run(RULE_NAME, rule, {
invalid: [
@@ -19,7 +19,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -40,7 +40,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data[1]",
},
@@ -61,7 +61,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data.at(1)",
},
@@ -83,7 +83,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data.at(index)",
},
@@ -105,7 +105,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data[index]",
},
@@ -127,7 +127,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data[index]",
},
@@ -156,7 +156,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data[index]",
},
@@ -183,7 +183,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data[index]",
},
@@ -211,7 +211,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "data.at(index)",
},
@@ -242,7 +242,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -266,7 +266,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -292,11 +292,11 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: { name: "setData1" },
},
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: { name: "setData2" },
},
],
@@ -320,11 +320,11 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: { name: "setData1" },
},
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: { name: "setData2" },
},
],
@@ -342,7 +342,7 @@ ruleTester.run(RULE_NAME, rule, {
}
`,
errors: [
- { messageId: "noDirectSetStateInUseEffect" },
+ { messageId: "noUnnecessaryUseEffect" },
],
},
{
@@ -361,7 +361,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -385,7 +385,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -407,7 +407,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -429,7 +429,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -451,7 +451,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -473,7 +473,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -493,7 +493,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -516,7 +516,7 @@ ruleTester.run(RULE_NAME, rule, {
// }
// `,
// errors: [
- // { messageId: "noDirectSetStateInUseEffect" },
+ // { messageId: "noUnnecessaryUseEffect" },
// ],
// },
// TODO: Add cleanup function check
@@ -536,7 +536,7 @@ ruleTester.run(RULE_NAME, rule, {
// }
// `,
// errors: [
- // { messageId: "noDirectSetStateInUseEffect" },
+ // { messageId: "noUnnecessaryUseEffect" },
// ],
// },
// TODO: Add cleanup function check
@@ -551,7 +551,7 @@ ruleTester.run(RULE_NAME, rule, {
// }
// `,
// errors: [
- // { messageId: "noDirectSetStateInUseEffect" },
+ // { messageId: "noUnnecessaryUseEffect" },
// ],
// },
{
@@ -566,7 +566,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -585,7 +585,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -607,7 +607,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -629,7 +629,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -651,7 +651,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -682,13 +682,13 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
},
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -724,7 +724,7 @@ ruleTester.run(RULE_NAME, rule, {
`,
errors: [
{
- messageId: "noDirectSetStateInUseEffect",
+ messageId: "noUnnecessaryUseEffect",
data: {
name: "setData",
},
@@ -945,5 +945,27 @@ ruleTester.run(RULE_NAME, rule, {
}, [])
}
`,
+ tsx`
+ function ProfileScreen({ navigation, route }) {
+ const [value, onChangeText] = React.useState(route.params.title);
+
+ React.useEffect(() => {
+ navigation.setOptions({
+ title: value === '' ? 'No title' : value,
+ });
+ }, [navigation, value]);
+
+ return (
+
+
+
+ );
+ }
+ `,
],
});
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts
similarity index 96%
rename from packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts
rename to packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts
index b2daa1468d..b4f45f37f5 100644
--- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts
@@ -13,7 +13,7 @@ import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
import { match } from "ts-pattern";
import { createRule } from "../utils";
-export const RULE_NAME = "no-direct-set-state-in-use-effect";
+export const RULE_NAME = "no-unnecessary-use-effect";
export const RULE_FEATURES = [
"EXP",
@@ -25,11 +25,13 @@ export default createRule<[], MessageID>({
meta: {
type: "problem",
docs: {
- description: "Disallow direct calls to the `set` function of `useState` in `useEffect`.",
+ description: "Disallow unnecessary use of 'useEffect'.",
[Symbol.for("rule_features")]: RULE_FEATURES,
},
messages: {
- noDirectSetStateInUseEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useEffect'.",
+ // TODO: Align the error messages precisely with the 6 scenarios described in react.dev/learn/you-might-not-need-an-effect.
+ noUnnecessaryUseEffect:
+ "You Might Not Need an Effect. Visit https://react.dev/learn/you-might-not-need-an-effect to learn how to remove unnecessary Effects.",
},
schema: [],
},
@@ -42,7 +44,7 @@ export function create(context: RuleContext): RuleListener {
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
return useNoDirectSetStateInUseEffect(context, {
onViolation(ctx, node, data) {
- ctx.report({ messageId: "noDirectSetStateInUseEffect", node, data });
+ ctx.report({ messageId: "noUnnecessaryUseEffect", node, data });
},
useEffectKind: "useEffect",
});
diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts
index 0a9a9ac3a8..599537f21c 100644
--- a/packages/plugins/eslint-plugin/src/configs/all.ts
+++ b/packages/plugins/eslint-plugin/src/configs/all.ts
@@ -49,6 +49,7 @@ export const rules = {
"@eslint-react/no-set-state-in-component-will-update": "warn",
"@eslint-react/no-string-refs": "error",
"@eslint-react/no-unnecessary-use-callback": "warn",
+ "@eslint-react/no-unnecessary-use-effect": "warn",
"@eslint-react/no-unnecessary-use-memo": "warn",
"@eslint-react/no-unnecessary-use-prefix": "warn",
"@eslint-react/no-unsafe-component-will-mount": "warn",
diff --git a/packages/plugins/eslint-plugin/src/configs/recommended.ts b/packages/plugins/eslint-plugin/src/configs/recommended.ts
index 0f93207d25..7d1e64b879 100644
--- a/packages/plugins/eslint-plugin/src/configs/recommended.ts
+++ b/packages/plugins/eslint-plugin/src/configs/recommended.ts
@@ -14,7 +14,6 @@ export const rules = {
...dom.rules,
...webApi.rules,
- "@eslint-react/hooks-extra/no-direct-set-state-in-use-effect": "warn",
"@eslint-react/naming-convention/context-name": "warn",
// "@eslint-react/naming-convention/use-state": "warn",
} as const satisfies RulePreset;
diff --git a/packages/plugins/eslint-plugin/src/configs/x.ts b/packages/plugins/eslint-plugin/src/configs/x.ts
index 9a704a3221..fb77283774 100644
--- a/packages/plugins/eslint-plugin/src/configs/x.ts
+++ b/packages/plugins/eslint-plugin/src/configs/x.ts
@@ -46,6 +46,9 @@ export const rules = {
"@eslint-react/no-set-state-in-component-did-update": "warn",
"@eslint-react/no-set-state-in-component-will-update": "warn",
"@eslint-react/no-string-refs": "error",
+ // "@eslint-react/no-unnecessary-use-callback": "warn",
+ "@eslint-react/no-unnecessary-use-effect": "warn",
+ // "@eslint-react/no-unnecessary-use-memo": "warn",
"@eslint-react/no-unnecessary-use-prefix": "warn",
"@eslint-react/no-unsafe-component-will-mount": "warn",
"@eslint-react/no-unsafe-component-will-receive-props": "warn",