Common React.js mistakes: Unneeded state
React.js has a tremendous amount of interest in communities interested in frontend development techniques. Those communities vary when it comes to average skill and knowledge about programming. It is not bad - the same situation is visible in Rails-centered communities (from where I’ve arrived) too.
That means there are many approaches to understand and use React.js ideas in practice. Certain patterns arise - most of them are cool, but there are also common mistakes made by developers trying to use React.js in their projects.
I want to speak about one of such common mistakes. It is a problem of unneeded state. In this blogpost I’d like to show you why it is bad and how to detect and avoid it in your React components.
Why state is used?
First of all there is a question why state is used in React components. It’s obvious, right? State is a subset of data that, if changed, causes a React component to re-render itself.
It is a good practice to keep state as minimal as possible. But why is that?
It’s because the more state you have, the more you need to keep in sync. Having big state can induce a big overhead in management of your component. It’s especially true if you have denormalized state - that mean your state has data that can be computed from the rest of state data. Everytime you update state, you need to remember to update denormalised fields too. This is a violation of the single source of truth principle.
Single Source of Truth
The single source of truth is a basic principle you need to have when thinking about what should go in to state and what shouldn’t. Keeping the only source of truth in your React components can save you a lot of headaches when working with React components.
To illustrate the problem let me take an example from our React.js by Example e-book. This example is about having two lists - the content of the second list is determined by the selection made in the first list. One of the most popular Polish web app for selling used cars called OtoMoto is using a similar widget. You choose the brand of a car you’re interested in and the list of models is updated accordingly.
This is how it looks like:
To model this situation in React.js, author decided to create two components - one called List
and the second called TwoLists
.
In one of the first versions the code looked like this:
class List extends React.Component {
render() {
let { name, items } = this.props;
let options = [];
options.push(<option value={name}>{name}</option>);
for(var index in items) {
let item = items[index];
options.push(<option value={item}>{item}</option>);
}
return (
<span>
<select onChange={this.props.handler} value={this.props.value ? this.props.value : "Model"}>
{options}
</select>
</span>
);
}
}
class TwoLists extends React.Component {
constructor(props) {
super(props);
this.state = {
brand: null, model: null,
models: [], buttonDisabled: true
};
this.brandChanged = this.brandChanged.bind(this);
this.modelChanged = this.modelChanged.bind(this);
this.buttonClicked = this.buttonClicked.bind(this);
this.knownModel = this.knownModel.bind(this);
}
brandChanged(event) {
let brand = event.target.value;
if(this.knownBrand(brand)) {
let models = this.data()[brand];
this.setState({
brand, model: null,
models: models, buttonDisabled: true,
});
} else {
this.setState({
brand: null, model: null,
models: [], buttonDisabled: true
});
this.setState({ brand: null, models: [] });
}
}
modelChanged(event) {
let model = event.target.value;
if(this.knownModel(model)) {
this.setState({ model, buttonDisabled: false });
} else {
this.setState({ model: null, buttonDisabled: true });
}
}
buttonClicked(event) {
let { brand, model } = this.state;
console.log(this.state);
console.log(`${brand} ${model} riding...`);
}
data() {
return (
{
'Opel': ['Agila', 'Astra', 'Corsa', 'Vectra'],
'Škoda': ['Fabia', 'Octavia', 'Superb', 'Yeti'],
'Toyota': ['Auris', 'Avensis', 'Corolla', 'Prius']
}
);
}
brands() {
return Object.keys(this.data());
}
knownBrand(brand) {
return this.brands().indexOf(brand) !== -1
}
knownModel(model) {
return this.state.models.indexOf(model) !== -1
}
render() {
return (
<div id={this.props.id}>
<List name="Brand" items={this.brands()} handler={this.brandChanged} value={this.state.brand} />
<List name="Model" items={this.state.models} handler={this.modelChanged} value={this.state.model} />
<button onClick={this.buttonClicked} disabled={this.state.buttonDisabled}>Ride</button>
</div>
);
}
}
(You can fiddle with this codeon this jsbin)
In this example the data are hard-coded in the data
method of TwoLists
React component. But the most interesting parts of this code are brandChanged
and modelChanged
methods.
First of all, they’re rather lengthy. They also set a lot of state at once. Take a look at the setState
s of brandChanged
method:
The first is:
this.setState({
brand, model: null,
models: models, buttonDisabled: true,
});
The second is:
this.setState({
brand: null, model: null,
models: [], buttonDisabled: true
});
If you think about it, there are two state fields that are unnecessary:
models
field. It can be derived frommodel
state field because it is always effectively athis.data()[this.state.brand]
or[]
.buttonDisabled
field. It is alwaysdisabled
when eitherbrand
ormodel
isnull
.
That means every time you want to update model
or brand
, you need to also update models
and buttonDisabled
!
If you forget about the proper setting models
or buttonDisabled
your component will broke. And the likelihood of forgetting this grows each time you’re adding state change to your component!
Did you hear the popular proverb of computer scientists that There are only two hard things in Computer Science: cache invalidation and naming things.? If so, you’ve just hit the first hard thing - denormalised fields in your state acts as cache and can be easily invalidated if you forget about updating them in all situations.
Also, since there are two sources of truth of ‘what models should be displayed’, which one is the true one? If you don’t follow the single source of truth principle you may quickly find yourself struggling with answering questions like that.
What can be done to improve the situation here? You need to remove models
and buttonDisabled
state fields and introduce functions that computes those fields dynamically!
Let’s introduce models
and buttonDisabled
methods. They’re relatively straightforward - the first one will look like this:
/* `models` field. It can be derived from `model` state field because it is always effectively a `this.data()[this.state.brand]` or `[]`. */
models() {
return this.state.brand ? this.data()[this.state.brand] : [];
}
And the second one:
/* `buttonDisabled` field. It is always `disabled` when both `brand` or `model` is `null`. */
buttonDisabled() {
return this.state.model === null || this.state.brand === null;
}
So far, so good. Now you need to replace all usages of this.state.buttonDisabled
and this.state.models
fields with newly created methods.
Replace render
of TwoLists
with the new version:
render() {
return (
<div id={this.props.id}>
<List name="Brand" items={this.brands()} handler={this.brandChanged} value={this.state.brand} />
<List name="Model" items={this.models()} handler={this.modelChanged} value={this.state.model} />
<button onClick={this.buttonClicked} disabled={this.buttonDisabled()}>Ride</button>
</div>
);
}
Also, replace knownModel
with the new version:
knownModel(model) {
return this.models().indexOf(model) !== -1
}
That’s it. Since there are no occurrences of this.state.models
and this.state.buttonDisabled
in code (I’ve used simple search in my editor, but things may get more complicated to search them all), it is safe to remove those state variables completely, along with simplifying all setState
s used in code.
Remove the unneeded part of initial state from constructor
:
this.state = {
brand: null, model: null
};
And from all setState
s:
brandChanged(event) {
let brand = event.target.value;
if(this.knownBrand(brand)) {
let models = this.data()[brand];
this.setState({
brand, model: null
});
} else {
this.setState({
brand: null, model: null
});
}
}
modelChanged(event) {
let model = event.target.value;
if(this.knownModel(model)) {
this.setState({ model });
} else {
this.setState({ model: null });
}
}
That’s it. You’ve removed unnecessary state from this component, making it easier to maintain. You’ve also simplified the state management part - setState
s are way simpler now.
Fiddle with the end result here on JSBin
How to avoid unneeded state
There are simple heuristics to check whether you have unneeded state or not. Here are few hints:
- If you find to make complex updates in
setState
and you set always two or more fields together, it’s likely that one of fields you’re setting is a denormalised form of the other state field. - If you introduce another field, think about what values it’ll take. It is a big chance that possible values are limited and can be enumerated - like there is only a possibility that this certain state field can have
"x"
,"y"
or"z"
value. If you find that there are fields that share this set of enumerated values or a certain subset, examine if such fields can be merged into one. - Of course think about if a component itself can change the field during its lifecycle - if it can’t, you can keep this field in props or as a constant within the component and not in state.
Those three simple tips can make wonders to avoid unneeded state in your components. I evaluate it as a checklist every time when I refactor my React components.
Checking unneeded state - the algorithm
Most of unneeded state can be checked in a structured way - there’s an algorithm for that. You can follow it if you want to ensure a certain state field x
is unneeded or not.
- Check the initialisation of
x
. Write down the initial value. - Take a look at all
setState
andreplaceState
calls in your component’s code. Write down all values thatx
can possibly have. - Take a look at those possible values. If there’s a variable inside the set of possible values, trace all assignments to this value as well as initial value and add those possible values of variable to the set. Remove the variable from your set.
- If there is a function/method call in your possible values set, perform an analysis of all possible
return
values of the function you’ve called. It can be defined in terms of function arguments only - so if there are local variables, remove it and add all possible assignments of this value to this set. Then, scan for all possible arguments in calls of this function and modify the possible values set accordingly. - Repeat the process until there are no variables or function calls in your possible values set. You can have only data primitives or state references (like
0
,new Foo(),
{},{ x: 2 }
orthis.state.??
) in this stage. - If your possible values set consists only of primitive neutral elements (
0
,{}
,null
,undefined
,[]
) and state references, this state field is unneeded.
It’s of course not suitable for performing it by hand - but you can try it in some simple cases as an exercise.
Summary
Unneeded state is a bad practice - a code smell if you like to name it that way - that can make work with your component classes harder. Try to think about avoiding and refactoring your component classes to not create unneeded state. Always remember about the single source of truth principle - it can make your component classes simpler to write and maintain. Remember that every denormalised state field is a possible vector of easy bugs.
In my practice I find this mistake quite often. I hope you’ll find it useful and write a better quality code for your components thanks to this blogpost. Happy coding!