JavaScript for PHP Geeks: ReactJS (with Symfony)


Immutability / Don't Mutate my State!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ok... so.... there's this annoying... but super important rule in React that we're totally violating. The rule is: the only time you're allowed to set or change the state property directly is when you're initializing it in the constructor. Everywhere else, you must call setState() instead of changing it directly.

Here's another way to say it: each piece of data on this.state should be immutable. And that is the part we're violating. It was really subtle! First, unlike PHP, in JavaScript arrays are objects. And so, like all objects, if you modify repLogs, that also modifies this.state.repLogs because... they're the same object!

And that's exactly what we did when we called repLogs.push: this changed, or mutated, the repLogs key on this.state! Yep! We changed the state before calling this.setState().

Do I really need to Avoid Mutating State?

Now, is that really a problem? I mean, everything seems to work. Basically... yes, but, honestly, it's subtle. There are two problems with mutating the state. First, setState() is actually asynchronous: meaning, React doesn't handle your state change immediately. For example, if two parts of your code called setState() at almost the same moment, React would process the first state change, re-render React, and then process the second state change. Because of this, if you mutate the state accidentally, it's possible that it will get overwritten in a way you didn't expect. It's unlikely, but we're trying to avoid a WTF moment.

The second reason is that if you mutate your state, it may prevent you from making some performance optimizations in the future.

Honestly, when you're learning React, the reasons for "why" you shouldn't mutate your state are hard to understand. The point is: you should avoid it, and we'll learn how. Well, if you're updating a scalar value like highlightedRowId, it's simple! But when your state is an object or an array, which, is an object, it's harder.

If you need to "add" to an array without, updating it, here's how: const newRepLogs = , create a new array, use ...this.state.repLogs to put the existing repLogs into it and then, add newRep. Yep, this is a new array: we did not change state. This solves our problem.

52 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 26
handleNewItemSubmit(itemLabel, reps) {
... lines 28 - 33
const newRepLogs = [...this.state.repLogs, newRep];
this.setState({repLogs: newRepLogs});
... lines 37 - 52

Using the setState() Callback

Except... there is one other tiny, annoying rule. Most of the time, when you set state, you set it to some new, specific value. But, if the new state depends on the old state - like our new repLogs depends on the current repLogs - then you need to use setState() as a callback.

Check it out: call this.setState(), but instead of passing data, pass a callback with a prevState argument. Inside, create the array: const newRepLogs = [...prevState.repLogs, newRep], and return the new state: repLogs set to newRepLogs.

56 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 26
handleNewItemSubmit(itemLabel, reps) {
... lines 28 - 34
this.setState(prevState => {
const newRepLogs = [...prevState.repLogs, newRep];
return {repLogs: newRepLogs};
... lines 41 - 56

Why the heck are we doing this? Remember how I said that setState() is asynchronous? Because of that, if you call setState() now, React may not use that state until a few milliseconds later. And, if something else added a new repLog between now and then... well... with our previous code, our new state would override and remove that new repLog!

I know, I know! Oof, again, it's subtle and probably won't bite you, and you'll probably see people skip this. To keep it simple, just remember the rule: if setting new state involves you using data on this.state, pass a callback instead. Then, you'll know you're safe.

Smarter Method & Prop Names

While we're here, something is bothering me. Our callback method is named handleNewItemSubmit(). But... we purposely designed RepLogApp so that it doesn't know or care that a form is being used to create rep logs. So let's rename this method: handleAddRepLog().

56 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 26
handleAddRepLog(itemLabel, reps) {
... lines 28 - 39
... lines 41 - 56

Yea. Make sure to also update the bind() call in the constructor. Below, when we pass the prop - update it here too. But... I think we should also rename the prop: onAddRepLog().

56 lines assets/js/RepLog/RepLogApp.js
... lines 1 - 6
constructor(props) {
... lines 8 - 19
this.handleAddRepLog = this.handleAddRepLog.bind(this);
... lines 22 - 41
render() {
... line 43
... lines 45 - 47
... line 50
... lines 52 - 56

And, if we change that, we need to update a few other spots: in RepLogs, change the propType. And, up where we destructure, PhpStorm is highlighting that this prop doesn't exist anymore. Cool! Change it to onAddRepLog, scroll down, and make the same change onAddRepLog={onAddRepLog}.

67 lines assets/js/RepLog/RepLogs.js
... lines 1 - 16
export default function RepLogs(props) {
const { withHeart, highlightedRowId, onRowClick, repLogs, onAddRepLog } = props;
... lines 19 - 24
return (
... lines 26 - 52
... line 56
... line 59
RepLogs.propTypes = {
... lines 61 - 63
onAddRepLog: PropTypes.func.isRequired,
... line 65

Repeat this process in RepLogCreator: rename the propType, update the variable name, and use the new function.

75 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 13
handleFormSubmit(event) {
... line 15
const { onAddRepLog } = this.props;
... lines 17 - 20
... lines 22 - 23
... lines 25 - 27
... lines 29 - 75

Oh, also, in RepLogs, the destructuring line is getting crazy long. To keep me sane, let's move each variable onto its own line.

73 lines assets/js/RepLog/RepLogs.js
... lines 1 - 16
export default function RepLogs(props) {
const {
} = props;
... lines 25 - 63
... lines 65 - 73

Moving the "itemOptions" onto a Property

Finally, we need to make one other small change. In RepLogCreator, all of our options are hardcoded. And, that's not necessarily a problem: we'll talk later about whether or not we should load these dynamically from the server.

But, to help show off some features we're about to work on, we need to make these a little bit more systematic. In the constructor, create a new property: this.itemOptions set to a data structure that represents the 4 items.

81 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 4
constructor(props) {
... lines 6 - 10
this.itemOptions = [
{ id: 'cat', text: 'Cat' },
{ id: 'fat_cat', text: 'Big Fat Cat' },
{ id: 'laptop', text: 'My Laptop' },
{ id: 'coffee_cup', text: 'Coffee Cup' },
... lines 17 - 18
... lines 20 - 81

Notice, I'm not making this props or state: we don't need these options to actually change. Nope, we're just taking advantage of the fact that we have a class, so, if we want to, we can store some data on it.

Back in render(), delete the 4 options and replace it with one of our fancy map structures: with an item argument. In the function, return an <option> element with value={}, key={} - we need that for any array of elements - and, for the text, use {option.text}.

81 lines assets/js/RepLog/RepLogCreator.js
... lines 1 - 36
render() {
return (
... lines 39 - 44
<select id="rep_log_item"
... lines 46 - 51
{ => {
return <option value={} key={}>{option.text}</option>
... lines 56 - 73
... lines 76 - 81

Nice! Let's make sure it works - refresh! It works and... yea - the options are still there.

When we submit... woh! All our state disappears! This smells like a Ryan bug, and it will be something wrong with how we're setting the state. Ah, yep! This should be prevState.repLogs.

Ok, try it again. Refresh, fill out the form and... we're good!

Let's talk about some validation!

Leave a comment!