React State Update During Render
ID |
javascript.react_set_state_in_render |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
react, reliability, state |
Description
Reports a React state update performed while the component is rendering: a useState setter called in the body of the function component, or this.setState called in a class component’s render().
Only an unconditional update is reported. Setting state during render behind a guard is a supported pattern for state derived from props, and it terminates because the guard stops being true. An update inside an event handler, an effect callback or any other nested function is not a render-phase update and is left alone.
Rationale
import React, { useState } from "react";
// Bad — the component never settles
function Counter() {
const [count, setCount] = useState(0);
setCount(count + 1);
return <span>{count}</span>;
}
// Bad — the class form of the same loop
class Panel extends React.Component {
render() {
this.setState({ open: true });
return <div />;
}
}
-
A state update asks React to render the component again. Performed while that render is still running, the new render runs the same line, which asks for another one.
-
React stops the function-component form with "Too many re-renders"; the class form spins until the tab stops responding. Neither is a slow path that can be tuned — the component simply does not work.
-
The mistake is easy to make while moving a line out of a handler, because the line itself is unchanged and only its position is wrong.
Remediation
Move the update to where React expects it: an event handler, an effect, or a lifecycle method. When the value is really derived from props, compute it during render instead of storing it, or guard the update so it stops.
import { useState } from "react";
// Good — the update belongs to an event
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// Good — derived value, no state at all
function Total({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0);
return <span>{total}</span>;
}
// Good — guarded derivation, which terminates
function Derived({ value }) {
const [previous, setPrevious] = useState(value);
if (value !== previous) {
setPrevious(value);
}
return <span>{previous}</span>;
}