Setting state inside render() leading to infinite loop

Loading

A dangerous React anti-pattern is calling state updaters (like setState or state setters from hooks) directly inside the render() method or function component body, which creates infinite re-render loops.

The Problem

// ❌ Class component infinite loop
class BadComponent extends React.Component {
  render() {
    this.setState({ count: 1 }); // Triggers re-render
    return <div>Count: {this.state.count}</div>;
  }
}

// ❌ Functional component infinite loop
function BadComponent() {
  const [count, setCount] = useState(0);
  setCount(1); // Triggers re-render
  return <div>Count: {count}</div>;
}

Why this happens:

  1. render() runs → calls state updater
  2. State update triggers re-render
  3. New render calls state updater again
  4. Infinite loop continues

Correct Solutions

1. Initialize State Properly

// ✅ Class component - set initial state in constructor
class GoodComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 1 }; // Initial state
  }
  render() {
    return <div>Count: {this.state.count}</div>;
  }
}

// ✅ Functional component - pass initial state to useState
function GoodComponent() {
  const [count] = useState(1); // Initial state
  return <div>Count: {count}</div>;
}

2. Update State in Event Handlers or Effects

// ✅ Class component - update in handler
class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <button onClick={this.increment}>
        Count: {this.state.count}
      </button>
    );
  }
}

// ✅ Functional component - update in useEffect or handler
function Counter() {
  const [count, setCount] = useState(0);

  // Or in useEffect if needed for side effects
  useEffect(() => {
    // Safe to set state here if properly conditioned
  }, [dependencies]);

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

When State Updates Are Safe in Render

There’s one exception where state updates during render are acceptable – when using the “derived state” pattern with getDerivedStateFromProps (class components) or calling setters during rendering in a way that doesn’t cause infinite loops:

// ✅ Acceptable - derived state pattern
class DerivedState extends React.Component {
  state = { derivedValue: 0 };

  static getDerivedStateFromProps(props, state) {
    // Calculate new state based on props
    return { derivedValue: props.someValue };
  }

  render() {
    return <div>{this.state.derivedValue}</div>;
  }
}

// ✅ Functional component equivalent
function DerivedState({ someValue }) {
  const [derivedValue, setDerivedValue] = useState(0);

  // Only update if prop changed
  if (someValue !== derivedValue) {
    setDerivedValue(someValue);
  }

  return <div>{derivedValue}</div>;
}

Common Infinite Loop Scenarios

  1. Data fetching in render:
   function UserProfile() {
     const [user, setUser] = useState(null);

     fetch('/user').then(r => r.json()).then(setUser); // ❌ Bad
     return <div>{user?.name}</div>;
   }
  1. Recursive state updates:
   function Counter() {
     const [count, setCount] = useState(0);
     setCount(count + 1); // ❌ Bad - infinite
     return <div>{count}</div>;
   }
  1. State based on props:
   function Display({ value }) {
     const [internal, setInternal] = useState(value);
     setInternal(value); // ❌ Bad - runs every render
     return <div>{internal}</div>;
   }

Best Practices

  1. Never call state updaters unconditionally in render
  2. Move side effects to useEffect (functional) or lifecycle methods (class)
  3. Use event handlers for user interaction-driven updates
  4. For derived state, compare props/state before updating
  5. Consider useMemo/useCallback for expensive calculations

Leave a Reply

Your email address will not be published. Required fields are marked *