Rules
no-direct-set-state-in-use-layout-effect
This rule is experimental and may change in the future or be removed. It is not recommended to use it in production code at this time.
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
🧪
Presets
recommendedrecommended-typescriptrecommended-type-checked
Description
Disallow direct calls to the set function of useState in useLayoutEffect.
Directly setting state in useLayoutEffect can lead to:
- Redundant state: You might be duplicating derived values that could be computed during render.
- Unnecessary effects: Triggering re-renders that could be avoided.
- Confusing logic: It can make component behavior harder to reason about.
What counts as a violation?
This is not allowed:
useLayoutEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);Instead, compute the value during render:
const fullName = firstName + " " + lastName;What is allowed?
The rule does not flag indirect calls, such as:
- Inside event handlers.
- Inside
asyncfunctions. - Inside
setTimeout,setInterval,Promise.then, etc.
Known limitations
-
It doesn’t check
setcalls inuseLayoutEffectcleanup functions.useLayoutEffect(() => { return () => { setFullName(firstName + " " + lastName); // ❌ Direct call }; }, [firstName, lastName]); -
It doesn’t detect
setcalls inasyncfunctions are being called before theawaitstatement.useLayoutEffect(() => { const fetchData = async () => { setFullName(data.name); // ❌ Direct call }; fetchData(); }, []);
Examples
The first three cases are common valid use cases because they are not called the set function directly in useLayoutEffect:
Passing
import { useLayoutEffect, useState } 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 { useLayoutEffect, useState } 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 { useLayoutEffect, useState } from "react";
export default function RemoteContent() {
const [content, setContent] = useState("");
useLayoutEffect(() => {
let discarded = false;
fetch("https://eslint-react.xyz/content")
.then((resp) => resp.text())
.then((text) => {
if (discarded) return;
setContent(text);
});
return () => {
discarded = true;
};
}, []);
return <h1>{count}</h1>;
}The following examples are derived from the React documentation:
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;
// ...
}Implementation
Further Reading
See Also
no-direct-set-state-in-use-effect
Disallow direct calls to thesetfunction ofuseStateinuseEffect.