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

Updating Deep State Data

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

Oh man, I let a bug crawl into our app. When we delete a rep log, it goes away, but, yuck, we get a big error:

Unexpected end of JSON input

This comes from rep_log_api.js. We call response.json()... which works great when the response is actually JSON. But, our delete endpoint returns nothing.

To fix this, we could create two different functions: one that decodes JSON and one that doesn't. But, I'll just make our code a bit fancier so it doesn't explode.

Use return response.text(): this returns a Promise where the data is the raw response content. Chain .then and use an arrow function with a text argument. Here, if text, return JSON.parse(text), else empty quotes.

... lines 1 - 4
.then(response => {
// decode JSON, but avoid problems with empty responses
return response.text()
.then(text => text ? JSON.parse(text) : '')
... lines 10 - 36

Go over, refresh and... delete! Ok, much better.

Success Message on Delete

We have this cool system where we can lift our cat 26 times and see this temporary success message. So, we might as well do the same thing when we delete. And this is easy! Inside of RepLogApp, down in handleDeleteRepLog, chain off the delete: .then(), an arrow function, and this.setSuccessMessage(): Item was Un-lifted.

... lines 1 - 76
handleDeleteRepLog(id) {
.then(() => {
this.setSuccessMessage('Item was Un-lifted!');
... lines 82 - 89
... lines 91 - 122

Cool! Move back and try it! Success... message.

"Ghosting" the Deleting Row

We could be satisfied with our loading & success message setup. But... if you want... we can get fancier! Right now, we delete the rep log state immediately, but we don't show the success message until after the AJAX call finishes. If you want that to feel more synchronized, we could move the setState() call so that it fires only when the rep log is actually deleted.

... lines 1 - 77
.then(() => {
// remove the rep log without mutating state
// filter returns a new array
this.setState((prevState) => {
return {
repLogs: prevState.repLogs.filter(repLog => repLog.id !== id)
this.setSuccessMessage('Item was Un-lifted!');
... lines 90 - 122

But, we're trading problems. Refresh again. When you click delete, there's a slight pause before the user gets any feedback. I'll add a few more items to the list real quick so that we can keep deleting.

Anyways, here's an idea of how we could improve this: when the user clicks delete, let's immediately change the opacity on the row that's being deleted, as a sort of "loading" indication.

Go into RepLogList: this is where we render the tr elements. So, imagine if there were a field on each repLog called isDeleting. If there were, we could say style={}, create an object, and set opacity: if isDeleting is true, use .3 else 1.

... lines 1 - 22
return (
... line 24
{repLogs.map((repLog) => (
... lines 27 - 29
opacity: repLog.isDeleting ? .3 : 1
... lines 34 - 42
... lines 44 - 55
... lines 57 - 67

This was easy. The interesting part of this problem is how we can add that new isDeleting field. Well, it looks simple at first: at the top of handleDeleteRepLog, before we call deleteRepLog(), we want to set the state of one of our rep logs to have isDeleting: true.

But... hmm... this is tricky. First, we need to find the one rep log by its id. Then, we need to set this flag, but without mutating that object or the array that it's inside of! Woh!

Here's the trick: use this.setState(), but pass it an arrow function with the prevState arg. We're doing this because our new state will depend on the old state. Return the new state we want to set, which is the repLogs key.

... lines 1 - 76
handleDeleteRepLog(id) {
this.setState((prevState) => {
return {
... lines 80 - 86
... lines 89 - 137

To not mutate the state, we basically want to create a new array, put all the existing rep logs inside of it, and update the one rep log... um... without actually updating it. Sheesh.

This is another one of those moments where you can understand why React can be so darn hard! But, the fix is easy, and it's an old friend: map! Use prevState.repLogs.map() with a repLog argument to the arrow function.

... lines 1 - 77
this.setState((prevState) => {
return {
repLogs: prevState.repLogs.map(repLog => {
... lines 81 - 85
... lines 89 - 137

The map function will return a new array, so that handles part of the problem. Inside, if repLog.id !== the id that's being deleted, just return repLog. And finally, we need to basically "clone" this last rep log and set the isDeleting flag on the new object. The way to do that is with return Object.assign() passing it an empty object, repLog, then the fields to update: isDeleting: true.

... lines 1 - 79
repLogs: prevState.repLogs.map(repLog => {
if (repLog.id !== id) {
return repLog;
return Object.assign({}, repLog, {isDeleting: true});
... lines 87 - 137

As I mentioned earlier, Object.assign() is like array_merge in PHP: the 3rd argument is merged into the second, and then that's merged into the first. The key is the strange first argument: the empty object. Thanks to that, we're creating a new object, and then all the data is merged into it. The repLog is not modified.

Phew! But... awesome! We've now learned how to add to an array, remove from an array, and even change an object inside an array, all without mutation. If your state structure is deeper than a simple object inside an array, it's probably too deep. In other words, you now know how to handle the most common, tough, state-setting situations.

Let's temporarily add a return statement below so we can really see if this is working. Ok, move over and refresh! Hit delete: that looks awesome! Our update worked perfectly.

Go back and remove the return.

Leave a comment!

Login or Register to join the conversation

6:30, a note: instead of Object.assign we can use the spread operator:
return {...item, newKey: newValue}


Hey plashenkov!

Good tip! The "Object Rest/Spread" is new to ES2018, so wasn't *quite* available when I recorded this (without some extra Babel config). But I believe it should be available now. It's *much* nicer - we talk about it a few chapters from now ;) https://symfonycasts.com/sc...



Yeah, already found that you speak about it later. Just finished the tutorial. It's awesome, thank you!

1 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