Documentation
Rules
hooks-extra/no-direct-set-state-in-use-effect

no-direct-set-state-in-use-effect

Rule category

Correctness.

What it does

This rule only checks for direct calls to the set function of useState in useEffect. It does not check for calls to set function in callbacks, event handlers, or Promise.then functions.

Disallow direct calls to the set function of useState in useEffect.

Why is this bad?

Calling setState directly in useEffect can lead to infinite update loops and other side effects in an imperceptible way.

Known limitations

  • The set call to useState in the cleanup function of useEffect will not be checked.
  • The current implementation does not support determining whether a set function called in an async function is actually at least one await after.

The limitation may be lifted in the future.

Examples

The first three cases are common valid use cases because they are not called the set function directly in useEffect.

Passing

import { useState, useEffect } from "react";
 
export default function Counter() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    window.addEventListener("click", () => setCount(c => c + 1));
    return () => window.removeEventListener("click", () => setCount(c => c + 1));
  }, []);
 
  return <h1>{count}</h1>;
}

Passing

import { useState, useEffect } 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 { useState, useEffect } from "react";
 
export default function RemoteContent() {
  const [content, setContent] = useState("");
 
  useEffect(() => {
    let discarded = false;
    fetch("https://example.com/content")
      .then(resp => resp.text())
      .then(text => {
        if (discarded) return;
        setContent(text);
      });
    return () => {
      discarded = true;
    };
  }, []);
 
  return <h1>{count}</h1>;
}

The following example is a case with three different depths of “Circular Effect” (aka. “Effect Loop”) in the component, where the useEffect hooks in each component will trigger each other infinitely. This rule helps you catch this kind of problem by flagging the direct set calls in them.
(A more specific rule for this is under development: no-circular-effect)

Failing

import { useEffect, useState } from "react";
 
/**
 * @component
 * @description CircularEffect1 has a circular effect with a depth of 1
 */
export function CircularEffect1() {
  const [items, setItems] = useState([0, 1, 2, 3, 4]);
 
  useEffect(() => {
    setItems(x => [...x].reverse());
  }, [items]);
 
  return null;
}
 
/**
 * @component
 * @description CircularEffect2 has a circular effect with a depth of 2
 */
export function CircularEffect2() {
  const [items, setItems] = useState([0, 1, 2, 3, 4]);
  const [limit, setLimit] = useState(false);
 
  useEffect(() => {
    setItems(x => [...x].reverse());
  }, [limit]);
 
  // ...Many other hooks between the two `useEffect` calls
 
  useEffect(() => {
    setLimit(x => !x);
  }, [items]);
  // ...
 
  return null;
}
 
/**
 * @component
 * @description CircularEffect3 has a circular effect with a depth of 3
 */
export function CircularEffect3() {
  const [items, setItems] = useState([0, 1, 2, 3, 4]);
  const [limit, setLimit] = useState(false);
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    setItems(x => [...x].reverse());
  }, [limit]);
 
  useEffect(() => {
    setCount(x => x + 1);
  }, [items]);
 
  useEffect(() => {
    setLimit(x => !x);
  }, [count]);
 
  return null;
}

For the examples below, the error message of this rule is less obvious in pointing out the problem, but it may also be helpful in exposing the problem until there is a dedicated rule to detect it.

Failing

import { useEffect, useState } from 'react';
 
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
 
  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

Passing

import { useState } from 'react';
 
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

Failing

import { useEffect, useState } from 'react';
 
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
 
  // 🔴 Avoid: redundant state and unnecessary Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);
 
  // ...
}

Passing

import { useMemo, useState } from 'react';
 
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Does not re-run getFilteredTodos() unless todos or filter change
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

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

Further Reading