Fix: Too many re-renders. React limits the number of renders to prevent an infinite loop.

The Error

You render a React component and your app crashes with this error:

Too many re-renders. React limits the number of renders to prevent an infinite loop.

Sometimes you also see this variant in the stack trace:

Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
    at renderWithHooks (react-dom.development.js)

This means your component is triggering a state update on every render, which causes another render, which triggers another state update — an infinite loop. React detects this after a threshold (usually around 50 consecutive renders) and kills the loop by throwing this error.

Why This Happens

React re-renders a component whenever its state or props change. If something inside the render body calls setState unconditionally, the cycle never stops:

  1. Component renders.
  2. During render, setState is called.
  3. State changes, so React schedules another render.
  4. Go to step 1.

The same loop happens when a useEffect updates state in a way that keeps triggering itself, or when you accidentally call a function in JSX instead of passing a reference to it.

Fix

1. Calling a Function in JSX Instead of Passing a Reference

This is the single most common cause. You write onClick={handleClick()} with parentheses, which calls the function during render instead of passing it as a callback.

Broken code:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

setCount(count + 1) executes immediately during render. It updates state, which triggers a re-render, which calls setCount again.

Fix — wrap it in an arrow function or pass a reference:

// Option A: arrow function
<button onClick={() => setCount(count + 1)}>

// Option B: function reference (no arguments)
<button onClick={handleClick}>

// Option C: updater function via arrow
<button onClick={() => setCount(prev => prev + 1)}>

The arrow function creates a callback that only runs when the button is clicked, not on every render.

This same mistake shows up with other handlers too:

// Wrong — calls immediately
<input onChange={handleChange(e)} />

// Right — passes a reference
<input onChange={handleChange} />

// Right — wraps in arrow function (if you need to pass extra args)
<input onChange={(e) => handleChange(e, someId)} />

2. Calling setState Directly in the Render Body

Any state update in the component body (outside of an event handler, useEffect, or callback) runs on every render and triggers an infinite loop.

Broken code:

function UserGreeting({ name }) {
  const [greeting, setGreeting] = useState('');

  // This runs on every render — infinite loop
  setGreeting(`Hello, ${name}!`);

  return <h1>{greeting}</h1>;
}

Fix — move it into a useEffect or derive the value directly:

// Option A: useEffect (when you actually need state)
function UserGreeting({ name }) {
  const [greeting, setGreeting] = useState('');

  useEffect(() => {
    setGreeting(`Hello, ${name}!`);
  }, [name]);

  return <h1>{greeting}</h1>;
}

// Option B: derive the value (no state needed at all)
function UserGreeting({ name }) {
  const greeting = `Hello, ${name}!`;
  return <h1>{greeting}</h1>;
}

Option B is almost always better. If a value can be computed from props or existing state, don’t put it in state. Derived values don’t need useState or useEffect. This eliminates the re-render loop entirely and simplifies your component. If you’re accessing properties on a potentially undefined value in your derived state, also watch out for Object is possibly undefined errors in TypeScript.

3. useEffect with Missing or Wrong Dependency Array

A useEffect without a dependency array runs after every render. If it calls setState, each state update triggers a new render, which triggers the effect again.

Broken code:

function DataLoader() {
  const [data, setData] = useState(null);

  // No dependency array — runs after every render
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(setData); // setState triggers re-render, which runs this effect again
  });

  return <pre>{JSON.stringify(data)}</pre>;
}

Fix — add a dependency array:

useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(setData);
}, []); // Empty array = runs once on mount

If you need the effect to re-run when specific values change, list those values:

useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(setData);
}, [userId]); // Re-runs only when userId changes

4. useEffect That Updates Its Own Dependency

This creates a subtler infinite loop. The effect runs, updates state, and that state is in the dependency array, so the effect runs again.

Broken code:

function ItemList({ items }) {
  const [sortedItems, setSortedItems] = useState([]);

  useEffect(() => {
    setSortedItems([...items].sort((a, b) => a.name.localeCompare(b.name)));
  }, [items, sortedItems]); // sortedItems is updated by this effect — infinite loop
}

Every time setSortedItems runs, sortedItems changes, which triggers the effect again.

Fix — remove the state you’re setting from the dependency array:

useEffect(() => {
  setSortedItems([...items].sort((a, b) => a.name.localeCompare(b.name)));
}, [items]); // Only re-run when items changes

Better yet, derive it with useMemo:

const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

No state, no effect, no loop.

5. Object or Array as useEffect Dependency (Reference Equality)

JavaScript compares objects and arrays by reference, not by content. If you create a new object or array on every render, useEffect sees a “new” dependency each time and runs again.

Broken code:

function UserProfile({ userId }) {
  const [profile, setProfile] = useState(null);

  // New object created every render — different reference each time
  const options = { includeAvatar: true, format: 'full' };

  useEffect(() => {
    fetch(`/api/users/${userId}`, { body: JSON.stringify(options) })
      .then(res => res.json())
      .then(setProfile);
  }, [userId, options]); // options is a new object every render — infinite loop
}

options is re-created on every render. Even though its content is identical, { includeAvatar: true } !== { includeAvatar: true } in JavaScript. The effect sees a new reference, runs again, calls setProfile, which triggers a re-render, which creates a new options object.

Fix — stabilize the reference with useMemo:

const options = useMemo(
  () => ({ includeAvatar: true, format: 'full' }),
  [] // Stable reference — only created once
);

useEffect(() => {
  fetch(`/api/users/${userId}`, { body: JSON.stringify(options) })
    .then(res => res.json())
    .then(setProfile);
}, [userId, options]); // options reference is stable now

Or move the object inside the effect:

useEffect(() => {
  const options = { includeAvatar: true, format: 'full' };
  fetch(`/api/users/${userId}`, { body: JSON.stringify(options) })
    .then(res => res.json())
    .then(setProfile);
}, [userId]); // No need to depend on options

Moving the object inside the effect is the cleanest solution when nothing outside the effect needs it.

The same issue applies to arrays, functions, and any non-primitive value:

// These all create new references on every render:
const tags = ['react', 'javascript'];       // new array each render
const config = { theme: 'dark' };           // new object each render
const format = (val) => val.toFixed(2);     // new function each render

Use useMemo for objects/arrays and useCallback for functions to keep references stable.

6. Callback Function as useEffect Dependency

Functions defined inside a component are re-created on every render. If you use one as a useEffect dependency, the effect runs every time.

Broken code:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  // New function reference every render
  const fetchResults = (q) => {
    fetch(`/api/search?q=${q}`)
      .then(res => res.json())
      .then(setResults);
  };

  useEffect(() => {
    fetchResults(query);
  }, [query, fetchResults]); // fetchResults changes every render
}

Fix — wrap the function with useCallback:

const fetchResults = useCallback((q) => {
  fetch(`/api/search?q=${q}`)
    .then(res => res.json())
    .then(setResults);
}, []); // setResults is stable, so no dependencies needed

useEffect(() => {
  fetchResults(query);
}, [query, fetchResults]); // fetchResults reference is now stable

Or define the function inside the effect:

useEffect(() => {
  const fetchResults = (q) => {
    fetch(`/api/search?q=${q}`)
      .then(res => res.json())
      .then(setResults);
  };
  fetchResults(query);
}, [query]);

7. Conditional State Update That Always Triggers

You add a condition around a setState call, but the condition is always true — so the “guard” doesn’t actually prevent the loop.

Broken code:

function Formatter({ text }) {
  const [formatted, setFormatted] = useState('');

  useEffect(() => {
    const result = text.trim().toLowerCase();
    // This condition is always true on the first pass, and the new value
    // is always "different" because formatted starts as ''
    if (result !== formatted) {
      setFormatted(result);
    }
  }, [text, formatted]); // formatted is a dependency AND gets set — loop

  return <p>{formatted}</p>;
}

On first render, formatted is '' and result is the processed text. They differ, so setFormatted runs. Now formatted changes, the effect fires again, but this time they match — so it stops. This might work, but adding formatted to the dependency array is unnecessary and risky. If the logic is more complex, it can loop forever.

Fix — remove the output state from dependencies:

useEffect(() => {
  setFormatted(text.trim().toLowerCase());
}, [text]); // Only depends on the input

Or derive it directly:

const formatted = text.trim().toLowerCase();

No state, no effect, no risk of looping.

8. setState in a Component That Renders a Parent

If a child component updates state that’s lifted to a parent, and the parent re-renders the child as a result, you can get a loop.

Broken code:

function Parent() {
  const [value, setValue] = useState('');
  return <Child onChange={setValue} />;
}

function Child({ onChange }) {
  // Calls onChange (which is setState) during render — infinite loop
  onChange('default');
  return <div>Child</div>;
}

Fix — move the call into useEffect:

function Child({ onChange }) {
  useEffect(() => {
    onChange('default');
  }, [onChange]);

  return <div>Child</div>;
}

Still Not Working?

Use React DevTools Profiler to Find the Loop

If you can’t identify which state update is causing the loop, the React DevTools Profiler can show you.

  1. Install React Developer Tools browser extension.
  2. Open DevTools and go to the Profiler tab.
  3. Click Record, then trigger the error.
  4. Look at which components re-render repeatedly and what caused each render (state update, props change, or parent re-render).

You can also add a console.log at the top of your component to see how many times it renders:

function MyComponent() {
  console.log('MyComponent rendered');
  // ...
}

If you see that message flooding the console, the loop is in that component. If the fetch call inside your effect is failing, make sure your localhost server is actually running.

Check for setState in Third-Party Library Callbacks

Some libraries call your callbacks synchronously during render. If your callback calls setState, you get a loop. Check if a third-party component is triggering your state update during its own render cycle.

// Some form libraries call validation functions during render
const validate = (values) => {
  // If this calls setState, you'll loop
  setErrors(checkErrors(values)); // Move this to an effect instead
  return checkErrors(values);
};

Verify You’re Not Passing a New Inline Object to a Memoized Component

React.memo prevents re-renders when props don’t change. But if you pass a new object literal as a prop, the reference changes every render and React.memo can’t help:

// This defeats React.memo — style is a new object every render
<MemoizedChild style={{ color: 'red' }} />

// Fix — stable reference
const style = useMemo(() => ({ color: 'red' }), []);
<MemoizedChild style={style} />

Check for State Updates in useLayoutEffect

useLayoutEffect runs synchronously after DOM mutations but before the browser paints. A setState inside useLayoutEffect triggers an immediate synchronous re-render. If that re-render triggers useLayoutEffect again, you get a loop that’s harder to debug because it happens before the browser even paints — your screen may freeze without any visible output.

Apply the same dependency array rules as useEffect. Don’t update state that’s also in the dependency array.

Check for Infinite Loops in Custom Hooks

If you’re using a custom hook that internally manages state and effects, the loop might be inside the hook. Trace into the hook’s source code and check if any of its useEffect calls update state that’s also in the dependency array.

// A buggy custom hook
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    // If this creates a new object every time and something upstream
    // depends on the size reference, it can cascade into a loop
    setSize({ width: window.innerWidth, height: window.innerHeight });
  }); // Missing dependency array — runs every render

  return size;
}

Add the missing [] dependency array, or use an event listener pattern:

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Runs once, updates on resize events only

  return size;
}

Related: Fix: React Hook is called conditionally | Fix: Hydration failed because the initial UI does not match what was rendered on the server | Fix: TypeError: Cannot read properties of undefined

Related Articles