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 touseState
in thecleanup
function ofuseEffect
will not be checked. - The current implementation does not support determining whether a
set
function called in anasync
function is actually at least oneawait
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;
// ...
}