Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

Immutability / Don't Mutate my State!

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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.

... 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.

... 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().

... 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().

... lines 1 - 6
constructor(props) {
... lines 8 - 19
this.handleAddRepLog = this.handleAddRepLog.bind(this);
}
... lines 22 - 41
render() {
... line 43
<RepLogs
... lines 45 - 47
onAddRepLog={this.handleAddRepLog}
/>
... 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}.

... lines 1 - 16
export default function RepLogs(props) {
const { withHeart, highlightedRowId, onRowClick, repLogs, onAddRepLog } = props;
... lines 19 - 24
return (
... lines 26 - 52
<RepLogCreator
onAddRepLog={onAddRepLog}
/>
... 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.

... lines 1 - 13
handleFormSubmit(event) {
... line 15
const { onAddRepLog } = this.props;
... lines 17 - 20
onAddRepLog(
... 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.

... lines 1 - 16
export default function RepLogs(props) {
const {
withHeart,
highlightedRowId,
onRowClick,
repLogs,
onAddRepLog
} = 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.

... 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: this.itemOptions.map() with an item argument. In the function, return an <option> element with value={option.id}, key={option.id} - we need that for any array of elements - and, for the text, use {option.text}.

... lines 1 - 36
render() {
return (
... lines 39 - 44
<select id="rep_log_item"
... lines 46 - 51
{this.itemOptions.map(option => {
return <option value={option.id} key={option.id}>{option.text}</option>
})}
</select>
... 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!

15
Login or Register to join the conversation
Helmi Avatar
Helmi Avatar Helmi | posted 3 years ago | edited

this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep]} ));

I'm a little confused, how prevState has been passed automatically during this call ?

1 Reply

Boom! Good find Helmi!

It's not really JavaScript magic (other than the arrow syntax stuff) - it's just that you can pass setState() an object (the normal use-case) OR a function that it will call where you then return the new state. When React calls that function, it passes you the previous state as the first argument.

For me, it's the syntax here that's the most confusing. Here is an expanded version (in case it helps anyone) of the setState() call:


// this code
this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep]} ));

// is basically equivalent to
this.setState(function(prevState) {
    return {
        repLogs: [...prevState.repLogs, newRep]
    };
});

Cheers!

1 Reply
Yurniel L. Avatar
Yurniel L. Avatar Yurniel L. | posted 2 years ago | edited

<i>Uncaught TypeError: Cannot read property 'setState' of undefined</i>

I don't know where is my error. Any help please?


export default class RepLogApp extends Component {
    constructor(props) {
        super(props);

        this.state = {
            highlightedRowId: null,
            repLogs: [
                { id: uuidv4(), reps: 25, itemLabel: "My Laptop", totalWeight: 112.5 },
                { id: uuidv4(), reps: 10, itemLabel: "Big fat Cat", totalWeight: 180 },
                { id: uuidv4(), reps: 4, itemLabel: "Big fat Cat", totalWeight: 72 }
            ]
        }
        
        this.handleRowMouseOver = this.handleRowMouseOver.bind(this);
        this.handleAppRepLog = this.handleAddRepLog.bind(this);
    }

    handleRowMouseOver(repLogId) {
        this.setState({highlightedRowId: repLogId});
    }

    handleAddRepLog(itemLabel, reps) {
        const newRep = {
            id: uuidv4(),
            itemLabel,
            reps,
            totalWeight: Math.floor(Math.random() * 50)
        };
        
        this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep] }) );
    }

    render() {
        return (
             // this line is being formatted (!)
            <RepLogs {...this.props} {...this.state} onRowMouseOver={this.handleRowMouseOver} onAddRepLog={this.handleAddRepLog} /> 
        );
    }
}

RepLogApp.propTypes = {
    withTitle: PropTypes.bool
};
Reply

Hey Yurniel L.

Hmm, the piece of code you pasted looks good to me. Could you check which line is causing that error?

Reply
Yurniel L. Avatar
Yurniel L. Avatar Yurniel L. | MolloKhan | posted 2 years ago | edited

Hi MolloKhan
This is the line whis causes the error on submitting the form:

this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep] }) );
Reply

hmm, your code looks good to me but I'd have to look inside the replogs component and see how it's using the onaddreplog function. Something is messing with the this variable

Reply
Yurniel L. Avatar

I finally saw my error :-o
In the constructor this.handleAppRepLog != this.handleAddRepLog.bind(this);

Thanks for your time!

Reply

Ha! that kind of errors are easy to overlook :)

Reply

Can someone tell me why
<br />this.setState(prevState => {repLogs: [...prevState.repLogs, newRep]});<br />
is not working? isn't this just a shorter version of
`
this.setState(prevState => {

return {repLogs: [...prevState.repLogs, newRep]};

});
`

I already apologize for the dumb question ;-)

Reply

Hey AndTheGodsMadeLove

You just hit an edge case. When you want to return an object using the short version, then you need to wrap the function body with parenthesis
this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep]} ));

Reply
GDIBass Avatar
GDIBass Avatar GDIBass | posted 5 years ago | edited

@5:35 Another way to do it is:


const newRepLogs = this.state.repLogs.slice(0).concat(newRep);

Slice creates a new object of the array. We can select a specific index or just pass 0 to get the whole array

Reply

Nice GDIBass! I think this is one of the tricks with all this immutability: find a few set of tools that make sense to YOU so that you don't go insane always trying to remember how to do these :). I like the ... syntax, but this one also makes a lot of sense to me.

Cheers!

1 Reply
GDIBass Avatar

Yeah I really like that ... syntax, too. I'm going to use that one from now on :)

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.9.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
        "friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.4
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/monolog-bundle": "^3.1", // v3.3.0
        "symfony/polyfill-apcu": "^1.0", // v1.9.0
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/swiftmailer-bundle": "^3.1", // v3.2.3
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/validator": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/twig": "2.10.*" // v2.10.0
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.6
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.5", // v1.5.0
        "symfony/phpunit-bridge": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0" // v4.1.4
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": {
        "@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
    },
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.5
        "@symfony/webpack-encore": "^0.26.0", // 0.26.0
        "babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
        "babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
        "bootstrap": "3", // 3.3.7
        "copy-webpack-plugin": "^4.4.1", // 4.5.1
        "core-js": "2", // 1.2.7
        "eslint": "^4.19.1", // 4.19.1
        "eslint-plugin-react": "^7.8.2", // 7.8.2
        "font-awesome": "4", // 4.7.0
        "jquery": "^3.3.1", // 3.3.1
        "promise-polyfill": "^8.0.0", // 8.0.0
        "prop-types": "^15.6.1", // 15.6.1
        "react": "^16.3.2", // 16.4.0
        "react-dom": "^16.3.2", // 16.4.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.0", // 7.3.1
        "sweetalert2": "^7.11.0", // 7.22.0
        "uuid": "^3.2.1", // 3.4.0
        "webpack-notifier": "^1.5.1", // 1.6.0
        "whatwg-fetch": "^2.0.4" // 2.0.4
    }
}
userVoice