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 ( + + +