DocumentationRulesno-direct-set-state-in-use-layout-effect

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

Full Name in @eslint-react/eslint-plugin

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

Features

🔍

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 set call to useState in the cleanup function of useLayoutEffect 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.

Further Reading