If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOk... 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()
.
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 |
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.
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 |
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!
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!
<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
};
Hey Yurniel L.
Hmm, the piece of code you pasted looks good to me. Could you check which line is causing that error?
Hi MolloKhan
This is the line whis causes the error on submitting the form:
this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep] }) );
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
I finally saw my error :-o
In the constructor this.handleAppRepLog != this.handleAddRepLog.bind(this);
Thanks for your time!
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 ;-)
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 parenthesisthis.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep]} ));
@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
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!
// 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
}
}
// 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
}
}
this.setState(prevState => ( {repLogs: [...prevState.repLogs, newRep]} ));
I'm a little confused, how prevState has been passed automatically during this call ?