react-hooks/set-state-in-effect

Disallow calling a state setter directly in the body of an effect

Also called react_hooks/set-state-in-effect, react/set-state-in-effect

An effect runs after React has rendered. If its body sets state right away, React renders the component a second time right after the first. Most of the time you don't need the state at all, because you can compute a value that depends only on props and other state while rendering.

The rule checks useEffect, useLayoutEffect and useInsertionEffect. Setting state later is fine, like in an event listener, a timer, or after a fetch().

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"react-hooks/set-state-in-effect" = "error" # or "warn"

Examples#

example.jsx1 problem
import { useEffect, useState } from "react";

function FullName({ first, last }) {
  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(first + " " + last);
  }, [first, last]);

  return <span>{fullName}</span>;
}

Moving the call into a helper doesn't change anything:

example.jsx1 problem
import { useEffect, useState } from "react";

function Search({ query }) {
  const [results, setResults] = useState([]);
  const clear = () => setResults([]);

  useEffect(() => {
    clear();
  }, [query]);

  return <List items={results} />;
}
example.jsxNo problems
function FullName({ first, last }) {
  const fullName = first + " " + last;
  return <span>{fullName}</span>;
}

This effect subscribes to something outside React, and only sets state when that changes:

example.jsxNo problems
import { useEffect, useState } from "react";

function OnlineStatus() {
  const [online, setOnline] = useState(true);

  useEffect(() => {
    const update = () => setOnline(navigator.onLine);
    window.addEventListener("online", update);
    return () => window.removeEventListener("online", update);
  }, []);

  return <span>{online ? "Online" : "Offline"}</span>;
}

The same goes for data that arrives later:

example.jsxNo problems
import { useEffect, useState } from "react";

function Profile({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/users/" + id)
      .then(response => response.json())
      .then(data => setUser(data));
  }, [id]);

  return <span>{user?.name}</span>;
}

You only know a ref's value after rendering, so storing it in state is allowed:

example.jsxNo problems
import { useLayoutEffect, useRef, useState } from "react";

function Tooltip({ text }) {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    setHeight(ref.current.offsetHeight);
  }, []);

  return <div ref={ref} style={{ top: -height }}>{text}</div>;
}