React Direct State Mutation
ID |
javascript.react_direct_state_mutation |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
react, reliability, state |
Description
Reports a write to this.state outside the constructor of a React class component: an assignment to one of its members, a replacement of the whole object, or an increment.
The constructor is exempt — assigning the initial this.state there is the documented way to create it. Only a class extending React’s component base (Component or PureComponent, however imported or qualified) is judged, because state is an ordinary member name and an ordinary class that keeps one is free to write it.
Rationale
// Bad
class Panel extends React.Component {
constructor(props) {
super(props);
this.state = { open: false, seen: 0 };
}
open() {
this.state.open = true;
this.state.seen++;
}
}
-
this.stateis a value React owns. Writing it directly changes the object without telling React, so no render is scheduled and the screen keeps showing the previous value. -
The change surfaces later, when some unrelated update renders the component — so the defect is intermittent by construction, and the line that caused it is nowhere near the render that shows it.
-
It also breaks the comparison React relies on:
shouldComponentUpdateandcomponentDidUpdateare handed two references to the same mutated object and cannot see what changed.
Remediation
Go through setState, which records the new value and schedules the render. Build a new object or array rather than editing the one in state.
// Good
open() {
this.setState({ open: true });
}
// Good — derived from the current value, so use the updater form
seen() {
this.setState(previous => ({ seen: previous.seen + 1 }));
}
// Good — a new array, so the comparison sees a new value
add(row) {
this.setState(previous => ({ rows: [...previous.rows, row] }));
}