npm.io
1.1.0 • Published 3d ago

eslint-plugin-react-you-might-not-need-an-effect

Licence
MIT
Version
1.1.0
Deps
0
Size
218 kB
Vulns
0
Weekly
0
Stars
1.9K

ESLint – React – You Might Not Need An Effect

NPM version NPM Downloads

ESLint (and Oxlint) plugin to catch unnecessary useEffects to make your React code easier to follow, faster to run, and less error-prone. Highly recommended for new React developers as you learn React's mental model. Even experienced developers will likely learn something new!

function Form() {
  const [firstName, setFirstName] = useState("Nick");
  const [lastName, setLastName] = useState("van Dyke");

  const [fullName, setFullName] = useState("");
  useEffect(() => {
    // ❌ Avoid storing derived state. Instead, compute "fullName" directly during render.
    // 📖 react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
    setFullName(firstName + " " + lastName);
  }, [firstName, lastName]);
}
  • Actionable guidance: Reports each anti-pattern with a specific suggested alternative and a docs link.
  • Deep analysis: Analyzes state, props, refs, and their upstream sources and context.
  • Dependency-aware: Considers when an effect runs to determine if its logic is actually redundant.
  • Edge-case obsessed: Focuses on unusual syntax and heuristics to keep the signal-to-noise ratio high.

React's eslint-plugin-react-hooks/set-state-in-effect rule flags synchronous setState calls inside effects, helping prevent unnecessary re-renders. However, unnecessary effects go far beyond this.

Installation

NPM

npm install --save-dev eslint-plugin-react-you-might-not-need-an-effect
Yarn
yarn add -D eslint-plugin-react-you-might-not-need-an-effect

Configuration

Flat Config
// eslint.config.js
import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";

export default [
  // Enable every rule as a warning
  reactYouMightNotNeedAnEffect.configs.recommended,
  // Or enable every rule as an error
  reactYouMightNotNeedAnEffect.configs.strict,
  // Or enable only specific rules
  {
    plugins: {
      reactYouMightNotNeedAnEffect,
    },
    rules: {
      "reactYouMightNotNeedAnEffect/no-derived-state": "warn",
    },
    languageOptions: {
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
  },
];
Legacy Config
// .eslintrc
{
  "extends": [
    // Enable every rule as a warning
    "plugin:react-you-might-not-need-an-effect/legacy-recommended",
    // Or enable every rule as an error
    "plugin:react-you-might-not-need-an-effect/legacy-strict",
  ],
  // Or enable only specific rules
  "plugins": ["react-you-might-not-need-an-effect"],
  "rules": {
    "react-you-might-not-need-an-effect/no-derived-state": "warn",
  },
  "env": {
    "browser": true,
  },
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true,
    },
  },
}
Oxlint

Use this plugin with Oxlint thanks to their JS plugin support!

// .oxlintrc.json
{
  "jsPlugins": ["eslint-plugin-react-you-might-not-need-an-effect"],
  "rules": {
    "react-you-might-not-need-an-effect/no-derived-state": "warn",
    "react-you-might-not-need-an-effect/no-chain-state-updates": "warn",
    "react-you-might-not-need-an-effect/no-event-handler": "warn",
    "react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "warn",
    "react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "warn",
    "react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "warn",
    "react-you-might-not-need-an-effect/no-pass-data-to-parent": "warn",
    "react-you-might-not-need-an-effect/no-external-store-subscription": "warn",
    "react-you-might-not-need-an-effect/no-initialize-state": "warn",
  },
  "env": {
    "browser": true,
  },
}
Suggested

Enforce these other rules in your codebase for more accurate analysis:

Rules

See the tests for extensive (in)valid examples for each rule.

no-derived-state

Disallow storing derived state in an effect:

function Form() {
  const [firstName, setFirstName] = useState("Nick");
  const [lastName, setLastName] = useState("van Dyke");

  const [fullName, setFullName] = useState("");
  useEffect(() => {
    // ❌ Avoid storing derived state. Instead, compute "fullName" directly during render.
    // 📖 react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
    setFullName(firstName + " " + lastName);
  }, [firstName, lastName]);
}
no-chain-state-updates

Disallow chaining state updates in an effect:

function Game() {
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  useEffect(() => {
    if (round > 10) {
      // ❌ Avoid chaining state changes. When possible, update "isGameOver" along with other relevant state simultaneously.
      // 📖 react.dev/learn/you-might-not-need-an-effect#chains-of-computations
      setIsGameOver(true);
    }
  }, [round]);
}
no-event-handler

Disallow using state and an effect as an event handler:

function Form() {
  const [dataToSubmit, setDataToSubmit] = useState();

  useEffect(() => {
    if (dataToSubmit) {
      // ❌ Avoid using state and effects as an event handler. Instead, call the code that uses "dataToSubmit" directly when the event occurs.
      // 📖 react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers
      submitData(dataToSubmit);
    }
  }, [dataToSubmit]);
}

Disallow using props and an effect as an event handler:

function ProductPage({ product, addToCart }) {
  useEffect(() => {
    if (product.isInCart) {
      // ❌ Avoid using props and effects as an event handler. Instead, move the code that uses "product" to the parent component.
      // 📖 react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);
}
no-adjust-state-on-prop-change

Disallow adjusting state in an effect when a prop changes:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  useEffect(() => {
    // ❌ Avoid adjusting state when a prop changes. Instead, adjust "selection" directly during render when "items" changes, or refactor your state to avoid this need entirely.
    // 📖 react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
    setSelection(null);
  }, [items]);
}
no-reset-all-state-on-prop-change

Disallow resetting all state in an effect when a prop changes:

function List({ items }) {
  const [selection, setSelection] = useState(null);

  useEffect(() => {
    // ❌ Avoid resetting all state when a prop changes. If "items" is a key, pass it as "key" instead so React will reset the component.
    // 📖 react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes
    setSelection(null);
  }, [items]);
}
no-pass-live-state-to-parent

Disallow passing live state to parents in an effect from a component:

function Child({ onTextChanged }) {
  const [text, setText] = useState();

  useEffect(() => {
    // ❌ Avoid passing live state to parents in an effect. Instead, lift "text" to the parent and pass it down to "Child" as a prop.
    // 📖 react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes
    onTextChanged(text);
  }, [onTextChanged, text]);
}

Disallow passing live state to parents in an effect from a custom hook:

const useCustomHook = ({ onTextChanged }) => {
  const [text, setText] = useState();

  useEffect(() => {
    // ❌ Avoid passing live state to parents in an effect. Instead, return "text" from "useCustomHook".
    // 📖 react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes
    onTextChanged(text);
  }, [onTextChanged, text]);
};
no-pass-data-to-parent

Disallow passing data to parents in an effect from a component:

function Child({ onDataFetched }) {
  const { data } = useQuery("/data");

  useEffect(() => {
    // ❌ Avoid passing data to parents in an effect. Instead, fetch "useQuery" in the parent and pass it down to "Child" as a prop.
    // 📖 react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent
    onDataFetched(data);
  }, [data, onDataFetched]);
}

Disallow passing data to parents in an effect from a custom hook:

const useCustomHook = ({ onFetched }) => {
  const data = useSomeAPI();

  useEffect(() => {
    // ❌ Avoid passing data to parents in an effect. Instead, return "useSomeAPI" from "useCustomHook".
    // 📖 react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent
    onFetched(data);
  }, [onFetched, data]);
};
no-external-store-subscription

Disallow subscribing to an external store in an effect:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();
    window.addEventListener("online", updateState);
    window.addEventListener("offline", updateState);
    return () => {
      // ❌ Avoid using an effect to subscribe to an external store. Instead, use "useSyncExternalStore" to manage "isOnline".
      // 📖 react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store
      window.removeEventListener("online", updateState);
      window.removeEventListener("offline", updateState);
    };
  }, []);
}
no-initialize-state

Disallow initializing state in an effect:

function Component() {
  const [state, setState] = useState();

  useEffect(() => {
    // ❌ Avoid initializing state in an effect. Instead, initialize "state"'s "useState()" with "Hello World". For SSR hydration, prefer "useSyncExternalStore".
    // 📖 tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store
    setState("Hello World");
  }, []);
}

Feedback

The ways to (mis)use an effect in real-world code are practically endless! This plugin is not exhaustive, but tries to be. If you encounter unexpected behavior or see opportunities for improvement, please open an issue or pull request. Your feedback helps improve the plugin for everyone!

Learn More

Deep-dives from the official React documentation:

Keywords