no-direct-set-state-in-use-layout-effect
Full Name in eslint-plugin-react-hooks-extra
react-hooks-extra/no-direct-set-state-in-use-layout-effectFull Name in @eslint-react/eslint-plugin
@eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effectFeatures
🔍
What it does
Disallow direct calls to the set function of useState in useLayoutEffect.
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.
Examples
The first three cases are common valid use cases because they are not called the set function directly in useLayoutEffect.
Passing
import { useState, useLayoutEffect } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
const handler = () => setCount(c => c + 1);
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
return <h1>{count}</h1>;
}Passing
import { useState, useLayoutEffect } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <h1>{count}</h1>;
}Passing
import { useState, useLayoutEffect } from "react";
export default function RemoteContent() {
const [content, setContent] = useState("");
useLayoutEffect(() => {
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>;
}Failing
import { useLayoutEffect, 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('');
useLayoutEffect(() => {
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 { useLayoutEffect, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 Avoid: redundant state and unnecessary Effect
const [visibleTodos, setVisibleTodos] = useState([]);
useLayoutEffect(() => {
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 { useLayoutEffect, useState } from 'react';
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Avoid: Resetting state on prop change in an Effect
useLayoutEffect(() => {
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 { useLayoutEffect, 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
useLayoutEffect(() => {
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;
// ...
}Known limitations
- The
setcall touseStatein thecleanupfunction ofuseLayoutEffectwill not be checked. - The current implementation does not support determining whether a
setfunction called in anasyncfunction is actually at least oneawaitafter.
The limitation may be lifted in the future.