Try @eslint-react/kit@beta
logoESLint React

set-state-in-effect

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

Full Name in eslint-plugin-react-x

react-x/set-state-in-effect

Full Name in @eslint-react/eslint-plugin

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

Presets

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

Rule Details

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.

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.

Examples

Transforming data in effects instead of render

When data can be derived directly from props, putting it in state inside an effect creates an unnecessary render cycle.

// Problem: transforming data in an effect
function Component({ rawData }) {
  const [processed, setProcessed] = useState([]);

  useEffect(() => {
    setProcessed(rawData.map(transform)); // extra render, should compute directly during rendering
  }, [rawData]);
}
// Recommended: compute directly during rendering
function Component({ rawData }) {
  const processed = useMemo(() => rawData.map(transform), [rawData]);
  // ...
}
// Recommended: simple computations can be done directly during rendering
function Component({ selectedId, items }) {
  const selected = items.find((i) => i.id === selectedId);
  return <div>{selected?.name}</div>;
}

Deriving state from props in effects

Synchronously setting state based on props in an effect is a common anti-pattern that causes extra renders.

// Problem: setting state based on props in an effect
function Component({ selectedId, items }) {
  const [selected, setSelected] = useState(null);

  useEffect(() => {
    setSelected(items.find((i) => i.id === selectedId));
  }, [selectedId, items]);
}
// Recommended: derive values during rendering
function Component({ selectedId, items }) {
  const selection = items.find((item) => item.id === selectedId) ?? null;
  // ...
}

Resetting state on prop changes

Using an effect to reset state when a prop changes causes an extra render. Instead, use the key prop to reset state automatically.

// Problem: resetting state in an effect
function ProfilePage({ userId }) {
  const [comment, setComment] = useState("");

  useEffect(() => {
    setComment(""); // extra render
  }, [userId]);
}
// Recommended: use the key prop to reset state automatically
function ProfilePage({ userId }) {
  return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
  const [comment, setComment] = useState("");
  // automatically resets when key changes
}

Setting synchronous loading state

Toggling a loading flag synchronously at the start of an effect causes an immediate re-render before any actual work begins.

// Problem: setting loading state synchronously
function Component() {
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true); // synchronously triggers extra render
    fetchData().then(() => setLoading(false));
  }, []);
}
// Recommended: initialize loading state to true so no synchronous setState is needed
function Component() {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchData().then(() => setLoading(false));
  }, []);
}

Indirect setState calls are allowed

The rule does not flag indirect calls, such as inside event handlers, async functions, or timers. These run outside of the synchronous effect execution and do not cause the same immediate re-render problem.

// OK: setState in an event handler
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const handler = () => setCount((c) => c + 1);
    window.addEventListener("click", handler);
    return () => window.removeEventListener("click", handler);
  }, []);
}
// OK: setState in an async function
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); // allowed in async callback
      });
    return () => {
      discarded = true;
    };
  }, []);
}

Known Limitations

Cleanup functions

The rule does not check set calls inside useEffect cleanup functions.

// Note: setState in a cleanup function is not flagged
function Component({ firstName, lastName }) {
  useEffect(() => {
    return () => {
      setFullName(firstName + " " + lastName); // not detected
    };
  }, [firstName, lastName]);
}

Async functions before await

The rule does not detect set calls in async functions that are called before the first await statement.

// Note: setState before await in an async function is not flagged
function Component({ data }) {
  useEffect(() => {
    const fetchData = async () => {
      setFullName(data.name); // not detected
      await syncToServer(data);
    };
    fetchData();
  }, [data]);
}

Options

Shared Settings

Custom state and effect hooks can also be configured via shared ESLint settings, which apply consistently across all rules in the plugin:

{
  "settings": {
    "react-x": {
      "additionalStateHooks": "/^(useMyState|useCustomState)$/u",
      "additionalEffectHooks": "/^(useMyEffect|useCustomEffect)$/u"
    }
  }
}

Versions

Resources

Further Reading


See Also

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

On this page