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:
render()
runs → calls state updater- State update triggers re-render
- New render calls state updater again
- 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
- 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>;
}
- Recursive state updates:
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1); // ❌ Bad - infinite
return <div>{count}</div>;
}
- State based on props:
function Display({ value }) {
const [internal, setInternal] = useState(value);
setInternal(value); // ❌ Bad - runs every render
return <div>{internal}</div>;
}
Best Practices
- Never call state updaters unconditionally in render
- Move side effects to
useEffect
(functional) or lifecycle methods (class) - Use event handlers for user interaction-driven updates
- For derived state, compare props/state before updating
- Consider useMemo/useCallback for expensive calculations