Skip to main content
These rules catch common mistakes with useState, useEffect, and related hooks that lead to bugs, performance issues, or unnecessary re-renders.

Rules

Severity: error
Rule ID: react-doctor/no-derived-state-effect
Detects state that is derived from props or other state inside useEffect. This pattern is almost always wrong.Why it’s bad:
  • Creates an extra render cycle
  • State updates happen after render instead of during
  • Can cause infinite loops if dependencies aren’t perfect
Bad:
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    setUser(fetchUser(userId));
  }, [userId]);
}
Good:
function UserProfile({ userId }) {
  const user = fetchUser(userId); // Compute during render
}
Or use a key to reset component state:
<UserProfile key={userId} />
Severity: error
Rule ID: react-doctor/no-fetch-in-effect
Prevents using fetch() directly inside useEffect. Manual data fetching leads to race conditions, missing loading states, and no caching.Why it’s bad:
  • Race conditions when component unmounts
  • No automatic deduplication or caching
  • Missing loading/error states
  • Waterfall requests
Bad:
useEffect(() => {
  fetch('/api/data').then(r => r.json()).then(setData);
}, []);
Good:
// Use a data fetching library
const { data } = useSWR('/api/data', fetcher);

// Or a server component in Next.js
async function Page() {
  const data = await fetch('/api/data');
  return <UI data={data} />;
}
Severity: warn
Rule ID: react-doctor/no-cascading-set-state
Flags useEffect containing 3 or more setState calls. This indicates state that should be managed together.Why it’s bad:
  • Multiple setState calls = multiple re-renders
  • Hard to keep state synchronized
  • Suggests missing abstraction
Bad:
useEffect(() => {
  setName(data.name);
  setEmail(data.email);
  setAge(data.age);
}, [data]);
Good:
// Use useReducer for related state
const [state, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
  dispatch({ type: 'SET_USER', payload: data });
}, [data]);
Severity: warn
Rule ID: react-doctor/no-effect-event-handler
Detects useEffect that runs only when a specific value changes and contains a single if statement. This is usually an event handler in disguise.Bad:
useEffect(() => {
  if (isOpen) {
    trackModal();
  }
}, [isOpen]);
Good:
function handleOpen() {
  setIsOpen(true);
  trackModal();
}
Severity: warn
Rule ID: react-doctor/no-derived-useState
Catches useState initialized directly from a prop value. If the prop changes, state won’t update.Bad:
function Form({ initialValue }) {
  const [value, setValue] = useState(initialValue);
  // initialValue changes won't update state!
}
Good:
// If you need to sync with prop changes:
function Form({ initialValue }) {
  const [value, setValue] = useState(initialValue);
  
  useEffect(() => {
    setValue(initialValue);
  }, [initialValue]);
}

// Or just derive it:
function Form({ value }) {
  return <input value={value} />;
}
Severity: warn
Rule ID: react-doctor/prefer-useReducer
Suggests using useReducer when a component has 5+ useState calls for related state.Bad:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [address, setAddress] = useState('');
const [phone, setPhone] = useState('');
Good:
const [state, dispatch] = useReducer(reducer, {
  name: '',
  email: '',
  age: 0,
  address: '',
  phone: ''
});
Severity: warn
Rule ID: react-doctor/rerender-lazy-state-init
Enforces lazy initialization for useState when initialized with a function call. The function runs on every render even though only the first result is used.Bad:
const [state, setState] = useState(expensiveComputation());
// expensiveComputation() runs on EVERY render
Good:
const [state, setState] = useState(() => expensiveComputation());
// Only runs once on mount
Severity: warn
Rule ID: react-doctor/rerender-functional-setstate
Requires using functional updates when the new state depends on the old state to avoid stale closures.Bad:
setCount(count + 1);
Good:
setCount(prev => prev + 1);
Severity: error
Rule ID: react-doctor/rerender-dependencies
Catches object literals and array literals in dependency arrays. These create new references every render, causing the effect to run infinitely.Bad:
useEffect(() => {
  doSomething();
}, [{ id: userId }]); // New object every render!
Good:
useEffect(() => {
  doSomething();
}, [userId]); // Primitive value

Build docs developers (and LLMs) love