Note: These notes are taken from Colt steele’s Modern React Bootcamp course
Setting State Using State
We know that setState()
is asynchronous…
So: it’s risky to assume previous call has finished when you call it. Also, React will sometimes batch (squash together) calls to setState
together into one for performance reasons. If a call to setState()
depends on current state, the safest thing is to use the alternate “callback form”.
setState Callback Form
this.setState(callback)
Instead of passing an object, pass it a callback with the current state as a parameter. The callback should return an object representing the new state.
1this.setState((curState) => ({ count: curState.count + 1 }));
Abstracting State Updates
The fact that you can pass a function to this.setState
lends itself nicely to a more advanced pattern called functional setState.
Basically you can describe your state updates abstractly as separate functions. But why would you do this?
1// elsewhere in the code
2function incrementCounter(prevState) {
3 return { count: prevState.count + 1 };
4}
5// somewhere in the component
6this.setState(incrementCounter);
Because testing your state changes is as simple as testing a plain function:
1expect(incrementCounter({ count: 0 })).toEqual({ count: 1 });
This pattern also comes up all the time in Redux!
Here is a nice opinionated article on the subject of using functional setState
Mutable Data Structures in State
Mutable Data Structures
We know how to set state to primitives: mainly numbers and strings. But component state also commonly includes objects, arrays, and arrays of objects.
1this.state = {
2 // store an array of todo objects
3 todos: [
4 { task: "do the dishes", done: false, id: 1 },
5 { task: "vacuum the floor", done: true, id: 2 },
6 ],
7};
You have to be extra careful modifying your array of objects!
1completeTodo(id) {
2 const theTodo = this.state.todos.find(t => t.id === id);
3 theTodo.done = true; // NOOOOO -> WRONG WAY
4
5 this.setState({
6 todos: this.state.todos // bad -> VERY BAD WAY TO SET LIKE THIS
7 });
8}
Why? It’s a long story…
Mutating nested data structures in your state can cause problems w/ React. (A lot of the time it’ll be fine, but that doesn’t matter. Just don’t do it!)
Immutable State Updates
A much better way is to make a new copy of the data structure in question. We can use any pure function to do this…
1completeTodo(id) {
2
3 // Array.prototype.map returns a new array
4 const newTodos = this.state.todos.map(todo => {
5 if (todo.id === id) {
6 // make a copy of the todo object with done -> true
7 return { ...todo, done: true };
8 }
9 return todo; // old todos can pass through
10 });
11
12 this.setState({
13 todos: newTodos // setState to the new array
14 });
15}
Pure functions such as .map
, .filter
, and .reduce
are your friends. So is the …
spread operator.
There is a slight efficiency cost due to the O(N) space/time required to make a copy, but it’s almost always worth it to ensure that your app doesn’t have extremely difficult to detect bugs due to mischevious side effects.
Immutable State Summary
- While it sounds like an oxymoron, immutable state just means that there is an old state object and a new state object that are both snapshots in time.
- The safest way to update state is to make a copy of it, and then call this.setState with the new copy.
- This pattern is a good habit to get into for React apps and required for using Redux.
Designing State
Designing the state of a React application (or any modern web app) is a challenging skill! It takes practice and time! However, there are some easy best-practices that we can talk about in this section to give you a jump-start.
Minimize Your State
In React, you want to try to put as little data in state as possible.
Litmus test
- does x change? If not,
x should not be part of state. It should be a prop. - is x already captured by some other value y in state or props? Derive it from there instead.
Bad Example of State Design
Let’s pretend we’re modelling a Person…
1this.state = {
2 firstName: "Matt",
3 lastName: "Lane",
4 birthday: "1955-01-08T07:37:59.711Z",
5 age: 64,
6 mood: "irate",
7};
- Does Matt’s first name or last name ever change? Not often I hope…
- Does Matt’s birthday ever change? How is that even possible!
- Matt’s age does change, however if we had
this.props.birthday
we could easily derive it from that. - Therefore, the only property here that is truly stateful is arguably mood (although Matt might dispute this 😉).
Fixed Example of State Design
1console.log(this.props);
2{
3 firstName: 'Matt',
4 lastName: 'Lane',
5 birthday: '1955-01-08T07:37:59.711Z',
6 age: 64
7}
8
9console.log(this.state);
10{
11 mood: 'insane'
12}
State Should Live On the Parent
Its better to support “downward data flow” philosophy of React. In general, it makes more sense for a parent component to manage state and have a bunch of “dumb” stateless child display components. This makes debugging easier, because the state is centralized. It’s easier to predict where to find state:
Is the current component stateless? Find out what is rendering it. There’s the state.
Todo Example:
1class TodoList extends Component {
2 constructor(props) {
3 super(props);
4 this.state = {
5 todos: [
6 { task: "do the dishes", done: false, id: 1 },
7 { task: "vacuum the floor", done: true, id: 2 },
8 ],
9 };
10 }
11 /* ... lots of other methods ... */
12 render() {
13 return (
14 <ul>
15 {this.state.todos.map((t) => (
16 <Todo {...t} />
17 ))}
18 </ul>
19 );
20 }
21}
TodoList is a smart parent with lots of methods, while the individual Todo items are just <li>
tags with some text and styling.