logoESLint React
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-effect

Full Name in @eslint-react/eslint-plugin

@eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effect

Features

🧪

Presets

  • recommended
  • recommended-typescript
  • recommended-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 async functions.
  • Inside setTimeout, setInterval, Promise.then, etc.

Known limitations

  • It doesn’t check set calls in useLayoutEffect cleanup functions.

    useLayoutEffect(() => {
      return () => {
        setFullName(firstName + " " + lastName); // ❌ Direct call
      };
    }, [firstName, lastName]);
  • It doesn’t detect set calls in async functions are being called before the await statement.

    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 { 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://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