logoESLint React
Rules

set-state-in-effect

Full Name in @eslint-react/eslint-plugin@beta

@eslint-react/set-state-in-effect

Full Name in eslint-plugin-react-x@beta

react-x/set-state-in-effect

Presets

x recommended recommended-typescript recommended-type-checked strict strict-typescript strict-type-checked

Description

Validates against setting state synchronously in an effect, which can lead to re-renders that degrade performance.

Setting state immediately inside an effect forces React to restart the entire render cycle. When you update state in an effect, React must re-render your component, apply changes to the DOM, and then run effects again. This creates an extra render pass that could have been avoided by transforming data directly during render or deriving state from props. Transform data at the top level of your component instead. This code will naturally re-run when props or state change without triggering additional render cycles.

Synchronous setState calls in effects trigger immediate re-renders before the browser can paint, causing performance issues and visual jank. React has to render twice: once to apply the state update, then again after effects run. This double rendering is wasteful when the same result could be achieved with a single render.

When something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering. This makes your code faster, simpler, and less error-prone.

In many cases, you may also not need an effect at all. Please see You Might Not Need an Effect for more information.

Common Violations

This rule catches several patterns where synchronous setState is used unnecessarily:

  • Transforming data in effects instead of render
  • Deriving state from props in effects
  • Setting loading state synchronously

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 useEffect cleanup functions.

    useEffect(() => {
      return () => {
        setFullName(firstName + " " + lastName); // ❌ Direct call
      };
    }, [firstName, lastName]);
  • It doesn’t detect set calls in async functions that are called before the await statement.

    useEffect(() => {
      const fetchData = async () => {
        setFullName(data.name); // ❌ Direct call
      };
      fetchData();
    }, []);

Examples

Passing

The following example from the React Docs demonstrates that setState in an effect is fine if the value comes from a ref, since it cannot be calculated during rendering:

import { useLayoutEffect, useRef, useState } from "react";

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);
  // ...
}

The following cases are common valid use cases because they do not call the set function directly in useEffect:

Passing

import { useEffect, useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const handler = () => setCount((c) => c + 1);
    window.addEventListener("click", handler);
    return () => window.removeEventListener("click", handler);
  }, []);

  return <h1>{count}</h1>;
}

Passing

import { useEffect, useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
    return () => clearInterval(intervalId);
  }, []);

  return <h1>{count}</h1>;
}

Passing

import { useEffect, useState } from "react";

export default function RemoteContent() {
  const [content, setContent] = useState("");

  useEffect(() => {
    let discarded = false;
    fetch("https://eslint-react.xyz/content")
      .then((resp) => resp.text())
      .then((text) => {
        if (discarded) return;
        setContent(text);
      });
    return () => {
      discarded = true;
    };
  }, []);

  return <h1>{count}</h1>;
}

TIP

If you need to fetch remote data within the component, consider using libraries like TanStack Query or SWR. They handle caching, re-fetching, and state management for you, making your code cleaner and more efficient.

The following examples are derived from the React Docs: set-state-in-effect and the React Docs: You Might Not Need an Effect:

Failing

import { useEffect, useState } from "react";

// ❌ Synchronous setState in effect
function Component({ data }) {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(data); // Extra render, use initial state instead
  }, [data]);
}

Failing

import { useEffect, useState } from "react";

// ❌ Setting loading state synchronously
function Component() {
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true); // Synchronous, causes extra render
    fetchData().then(() => setLoading(false));
  }, []);
}

Failing

import { useEffect, useState } from "react";

// ❌ Transforming data in effect
function Component({ rawData }) {
  const [processed, setProcessed] = useState([]);

  useEffect(() => {
    setProcessed(rawData.map(transform)); // Should derive in render
  }, [rawData]);
}

Passing

import { useMemo, useState } from "react";

// ✅ Calculate during render
function Component({ rawData }) {
  const processed = useMemo(() => rawData.map(transform), [rawData]);
  // ...
}

Failing

import { useEffect, useState } from "react";

// ❌ Deriving state from props
function Component({ selectedId, items }) {
  const [selected, setSelected] = useState(null);

  useEffect(() => {
    setSelected(items.find((i) => i.id === selectedId));
  }, [selectedId, items]);
}

Passing

// ✅ Calculate during render
function Component({ selectedId, items }) {
  const selected = items.find((i) => i.id === selectedId);
  return <div>{selected?.name}</div>;
}

Failing

import { useEffect, useState } from "react";

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState("");

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment("");
  }, [userId]);
  // ...
}

Passing

import { useState } from "react";

export default function ProfilePage({ userId }) {
  return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState("");
  // ...
}

Failing

import { useEffect, 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
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

Passing

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);
  }
  // ...
}
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

Further Reading


See Also

  • set-state-in-render
    Validates against unconditionally setting state during render, which can trigger additional renders and potential infinite render loops.

On this page