Skip to content

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

Repository files navigation

ESLint - React - You Might Not Need An Effect

NPM version NPM Downloads

ESLint plugin to catch when You Might Not Need An Effect (and more) to make your code easier to follow, faster to run, and less error-prone. Highly recommended for new React developers as you learn its mental model, and even experienced developers may be surprised!

  • Actionable fixes: Reports specific anti-patterns, with suggestions and links.
  • Deep analysis: Analyzes state, props, refs, and their upstream sources.
  • 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 official eslint-plugin-react-hooks/set-state-in-effect rule flags synchronous setState calls inside effects, helping prevent unnecessary re-renders. However, unnecessary effects aren’t limited to this, as I'm sure we've all seen (or written 😅).

📦 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

Recommended

Add the plugin's recommended config to your ESLint configuration file to enable every rule as a warning.

Experimentally, use the strict config instead to enable every rule as an error.

Legacy config (.eslintrc)

{
  "extends": [
    "plugin:react-you-might-not-need-an-effect/legacy-recommended",
    // or
    "plugin:react-you-might-not-need-an-effect/legacy-strict",
  ],
}

Flat config (eslint.config.js)

import reactYouMightNotNeedAnEffect from "eslint-plugin-react-you-might-not-need-an-effect";

export default [
  reactYouMightNotNeedAnEffect.configs.recommended,
  // or
  reactYouMightNotNeedAnEffect.configs.strict,
];

Custom

If not using an included config, manually set your languageOptions:

import globals from "globals";

// ...
{
  globals: {
    ...globals.browser,
  },
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
  },
};

Suggested

Consider enforcing these rules in your codebase for more accurate analysis.

🔎 Rules

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

no-derived-statedocs

Disallow storing derived state in an effect:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  const [fullName, setFullName] = useState('');
  useEffect(() => {
    // ❌ Avoid storing derived state. Compute "fullName" directly during render, optionally with `useMemo` if it's expensive.
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
}

Disallow storing state derived from any state (even external) when the setter is only called once:

function Form() {
  const prefix = useQuery('/prefix');
  const [name, setName] = useState();
  const [prefixedName, setPrefixedName] = useState();

  useEffect(() => {
    // ❌ Avoid storing derived state. "prefixedName" is only set here, and thus could be computed directly during render.
    setPrefixedName(prefix + name)
  }, [prefix, name]);
}

no-chain-state-updatesdocs

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 all relevant state simultaneously.
      setIsGameOver(true);
    }
  }, [round]);
}

no-event-handlerdocs

Disallow using state and an effect as an event handler:

function ProductPage({ product, addToCart }) {
  useEffect(() => {
    if (product.isInCart) {
      // ❌ Avoid using state and effects as an event handler. Instead, call the event handling code directly when the event occurs.
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);
}

no-adjust-state-on-prop-changedocs

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 the state directly during render, or refactor your state to avoid this need entirely.
    setSelection(null);
  }, [items]);
}

no-reset-all-state-on-prop-changedocs

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.
    setSelection(null);
  }, [items]);
}

no-pass-live-state-to-parentdocs

Disallow passing live state to parents in an effect:

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

  useEffect(() => {
    // ❌ Avoid passing live state to parents in an effect. Instead, lift the state to the parent and pass it down to the child as a prop.
    onTextChanged(text);
  }, [onTextChanged, text]);
}

no-pass-data-to-parentdocs

Disallow passing data to parents in an effect:

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

  useEffect(() => {
    // ❌ Avoid passing data to parents in an effect. Instead, let the parent fetch the data itself and pass it down to the child as a prop.
    onDataFetched(data)
  }, [data, onDataFetched]);
}

no-pass-ref-to-parentdocs

Disallow passing refs to parents in an effect.

function Child({ onRef }) {
  const ref = useRef();

  useEffect(() => {
    // ❌ Avoid passing refs to parents in an effect. Use `forwardRef` instead.
    onRef(ref.current);
  }, [onRef, ref.current]);
}

Disallow calling props inside callbacks registered on refs in an effect.

const Child = ({ onClicked }) => {
  const ref = useRef();
  useEffect(() => {
    ref.current.addEventListener('click', (event) => {
      // ❌ Avoid calling props inside callbacks registered on refs in an effect. Use `forwardRef` to register the callback in the parent instead.
      onClicked(event);
    });
  }, [onClicked]);
}

Disallow receiving refs from parents to use in an effect.

const Child = ({ ref }) => {
  useEffect(() => {
    // ❌ Avoid receiving refs from parents to use in an effect. Use `forwardRef` instead.
    ref.current.addEventListener('click', (event) => {
      console.log('Clicked', event);
    });
  }, [ref]);
}

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()`.
    setState("Hello World");
  }, []);
}

no-empty-effect

Disallow empty effects:

function Component() {
  // ❌ This effect is empty and could be removed.
  useEffect(() => {}, []);
}

💬 Feedback

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

📖 Learn More

About

Catch unnecessary React effects to make your code simpler, faster, and safer.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

No packages published

Contributors 3

  •  
  •  
  •