Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

...Object Rest Spread

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

When a user submit an invalid form, we get a nice error message... but our cool "lifting to the database" message stays! Totally confusing! It looks like we're still trying to save the new rep log. Let's fix that!

In RepLogApp, the state that controls this is called isSavingNewRepLog. When the AJAX call is successful, we set this back to false. We need to also set this back to false inside the catch. And yes, fixing this is as easy as just copying this key and pasting it below. Sure, this would duplicate that key in two places... but, that's super-minor duplication: no big deal.

Except... I want to learn a super-fun new language feature. To show that, let's fix this in a slightly fancier way. Above the AJAX call, set const newState = an object with savingNewRepLog set to false.

... lines 1 - 45
handleAddRepLog(item, reps) {
... lines 47 - 55
const newState = {
isSavingNewRepLog: false
};
... lines 59 - 82
}
... lines 84 - 148

This represents the new state that we want to apply in all situations: success or failure. In other words, we want to merge this state into whatever is being set in success and also down in catch.

How can you merge objects in JavaScript? We've seen it before: Object.assign(). Check it out: return Object.assign(). For the first argument, copy the new state and paste. For the second argument, use newState.

... lines 1 - 58
createRepLog(newRep)
.then(repLog => {
this.setState(prevState => {
... lines 62 - 63
return Object.assign({
repLogs: newRepLogs,
newRepLogValidationErrorMessage: '',
}, newState);
});
... lines 69 - 70
})
... lines 72 - 148

Object.assign() will merge the data from newState into the first object and return it. Perfect!

Repeat this in catch: add Object.assign(), then newState.

... lines 1 - 71
.catch(error => {
error.response.json().then(errorsData => {
... lines 74 - 76
this.setState(Object.assign({
newRepLogValidationErrorMessage: firstError
}, newState));
})
})
... lines 82 - 148

Let's go make sure this works: refresh, select our bad data and... cool. It shows for just a second, then disappears.

Installing & Configuring babel-plugin-transform-object-rest-spread

Object.assign() is really great. We also used it earlier to merge two objects without modifying the original object. That was super important.

The only problem with Object.assign() is that it's... kinda confusing to look at, especially if you need to use it to avoid mutation.

Ok, idea time: what if we could do this: remove the Object.assign(), return a normal object, but then, add ...newState.

... lines 1 - 59
.then(repLog => {
this.setState(prevState => {
... lines 62 - 63
return {
...newState,
... lines 66 - 67
}
});
... lines 70 - 71
})
... lines 73 - 150

That would be cool, right? I mean, we already do this for arrays! But... Webpack explodes: the "spread" syntax does not work for objects.

Or does it?! Google for "babel plugin transform object rest spread" and find the Babel documentation page. The feature we're "dreaming" about is called "object rest spread". It is not an official ECMAScript feature. But, it's currently a proposed, "draft" feature that's in a late stage. There's no promises, but that means it will likely become a real feature in a future ECMAScript version.

And, because the JS world is a bit nuts, you don't need to wait! We can teach Babel how to understand this syntax. Copy the package name, find your terminal and run:

yarn add @babel/plugin-proposal-object-rest-spread --dev

Oh, and as I mentioned before, most of these Babel plugins will have a slightly new name in the future: @babel/plugin-transform-object-rest-spread. But, it's really the same library.

When you work with Babel, you typically configure it with a .babelrc file. But, Encore does this for us! Open webpack.config.js: the configureBabel() function allows us to extend its configuration. Add babelConfig.plugins.push() and paste the name.

Tip

If you downloaded the code (and on newer projects) make sure you installed the newer library - @babel/plugin-proposal-object-rest-spread - and use that name here instead.

... lines 1 - 3
Encore
... lines 5 - 29
.configureBabel((babelConfig) => {
... lines 31 - 36
babelConfig.plugins.push('transform-object-rest-spread');
})
... lines 39 - 43

In the future, if you download the new @babel/plugin-transform-object-rest-spread library, the plugin name will be the full library name, starting with the @babel part. Just follow the docs.

Head back to the tab that's running Encore. Yep, it's super angry. Stop and re-run this command:

yarn run encore dev-server

And... it works! That's awesome! Babel now understands this syntax.

But... PhpStorm is still angry: ESLint parsing error. No worries: we just need to tell ESLint that this syntax is cool with us. Open .eslintrc.js. Under ecmaFeatures, add experimentalObjectRestSpread set to true.

Tip

On ESLint version 6 or higher, you only need to change the ecmaVersion to 2018. You do not need to add the experimentalObjectRestSpread option because it's already enabled.

21 lines .eslintrc.js
module.exports = {
... line 2
parserOptions: {
... lines 4 - 5
ecmaFeatures: {
... line 7
experimentalObjectRestSpread: true
}
},
... lines 11 - 19
};

Deep breath: go back to RepLogApp. And... sweet! The error is gone!

Using the Object Rest Spread

Let's finish this! Down in catch, remove Object.assign(), remove the second argument and add ...newState.

... lines 1 - 72
.catch(error => {
error.response.json().then(errorsData => {
... lines 75 - 77
this.setState({
...newState,
... line 80
});
})
})
... lines 84 - 150

And one more time: scroll down to handleDeleteRepLog(). We don't need this weird code anymore! Just return a new object with ...repLog then isDeleting: true.

... lines 1 - 92
handleDeleteRepLog(id) {
this.setState((prevState) => {
return {
repLogs: prevState.repLogs.map(repLog => {
... lines 97 - 100
return {...repLog, isDeleting: true};
})
};
});
... lines 105 - 117
}
... lines 119 - 150

I love that. And even better, when we refresh, it's not broken! We rock!

Leave a comment!

10
Login or Register to join the conversation
xmontero Avatar
xmontero Avatar xmontero | posted 1 month ago | edited

I'm rather new to javascript advanced features and compiling transpiled language thru babel... so, maybe my question seems a bit silly to all you.

Thing is...

I found that object spread "seems" to work for my setup with no plugins. In fact, I can see that Object Spread seems to be super-common since the last years according to mozilla developer docs:

Section "Spread in object literals": https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals

There's a table below (section "Browser compatibility") and it seems to be compatible nearly at any browser or server with node with v8.3.0 or above.

My node version is v18 so it seems natural I have the object spread there:

ubuntu@devel-sf6:/files/repos$ node --version
v18.9.1

Okey... Now what?

ES Lint complains. You say to set "version to 2018" in ES configuration in the video card.

But for curiosity I went to google and searched [eslint ecma version 2018] and I found this: https://eslint.org/docs/latest/user-guide/configuring/language-options

And it seems that more than 6 and 2018 there are myriads of ecma versions.

# I have 2 questions here:

a) How do I ask my system "hey, what ECMA standard are you?"
b) Where exactly do I configure that in the .eslintrc.js

About question "a" I wonder if I should set 2018, or any of the newer, 2019, 2020, 2021 or even 2022 as stated in the eslint page.

On the other hand, the reason by which I ask for question "b" is that in .eslintrc.jc I see both

parserOptions: {
    ecmaVersion: 6,
    [...]

As welll as

env: {
    es6: true,
    [...]

which for me is redundant as they both indicate "ecma 6" and I don't really know what config option controls what in ES Lint.

# In short

  1. I seems it's working out of the box in my webpack.
  2. Still my linter needs to be taught about this, but there are infinite versions and 2 places where to set it and I'm lost.

So... What exact syntax should I use in ES Lint config file and what exact ECMA version should I set?

Reply

Hey @xmontero!

No worries - this stuff can be confusing. I focus a LOT on ES6, as MOST modern language features come from that version. But, in truth, they release a new version pretty much every year - the naming is now done by the year. So, the object spread came from ECMAScript 2018, for example. When I first made this tutorial, that was super new stuff. Now it's OLD news and is supported everywhere. That's why things are working out-of-the-box for you.

Still my linter needs to be taught about this, but there are infinite versions and 2 places where to set it and I'm lost.

Yea, this is confusing. In short, it seems best to set both of these Well, actually, only one is needed. Let... me break this into 2 parts:

A) What version to set these to? If you're using a transpiler like Babel, then you might as well use all of the latest and greatest language features - as these will be rewritten to older JavaScript as needed for the browsers you want to support. But even browsers are quite good these days at supporting most modern JS. Anyways, I'd set env.es2022: true - source https://eslint.org/docs/latest/user-guide/configuring/language-options#specifying-environments - again, the reason being that you might as well use the latest stuff.

B) Setting env.es2022 will automatically set parserOptions.ecmaVersion to 2022. So in reality, you're setting both (though you should only need to set env.es2022. Why do both need to be set internally? What's the difference? parserOptions.ecmaVersion is about syntax: if es2022 introduced some new syntax (e.g. like let and const in es6/es2015), then this tells eslint to support that. env.es2022 is about what global variables you have access to. So, if es2022 introduced some new object (e.g. like Promise in es6/es2015), then this would tell eslint to support that.

Let me know if this helps clear things up - eslint is fun to have once it's set up :).

Cheers!

Reply
John christensen Avatar
John christensen Avatar John christensen | posted 1 year ago

I had a terrible time getting the object rest spread plugin to work. Apparently the new versions of this library requires Babel 7, so you need to upgrade webpack-encore itself.

This is how I did that:

1) yarn remove babel-preset-react

2) In package.json, change the version of webpack-encore to "^0.21.0" and then run yarn install

3) yarn add @babel/preset-react --dev

4) And finally: yarn add @babel/plugin-proposal-object-rest-spread --dev

Now yarn watch should compile without errors.

When you get to the step about updating Babel configuration, be sure to use this:

babelConfig.plugins.push('@babel/plugin-proposal-object-rest-spread');

And as mention by others, you no longer have to change anything in eslintrc.js.

If you want to know more about the changes in Encore 0.21.0, see Ryan's blog post here:

https://symfony.com/blog/en...

Hope this helps someone else!

Reply

Hey John christensen!

Ah! I'm really sorry about the trouble! But thanks so much for the awesome details! I'm going to also take a look into this (using your notes) to see what note we could add to the tutorial to help others.

Anyways, I really appreciate your comment - it's a tough challenge for us (and we're always trying to improve to find things before users) to add notes to keep tutorials working as new versions of things come out :).

Cheers!

Reply
Carlos G. Avatar
Carlos G. Avatar Carlos G. | posted 2 years ago

ESLint version 6 don't work with "experimentalObjectRestSpread: true,".

Fix:
{
"parserOptions": {
"ecmaVersion": 2018
}
}

https://eslint.org/docs/user-guide/migrating-to-6.0.0#the-deprecated-experimentalobjectrestspread-option-has-been-remo

Reply

Thanks Carlos G.!

We'll get a not added for people using newer versions of eslint! The new way is easier anyways ;)

Cheers!

1 Reply
Ryan S. Avatar
Ryan S. Avatar Ryan S. | posted 3 years ago

trying to follow but ran into issues. ended up installing newer: yarn add @babel/plugin-proposal-object-rest-spread --dev
but this required @babel/core so yarn add @babel/core --dev
and then yarn add @babel/preset-react --dev

then removed the old version: yarn remove babel-preset-react

but now i get: Error: Install babel-preset-react to use enableReactPreset()

How can I tell webpack encore to use the newer version of preset-react?

EDIT: I was able to back everything out and get it working. Just caution to those that might try to install the @babel/plugin instead of the one you use.

Reply

Hey Stinndler,

Thank you for sharing some tips with others! I suppose when you installed a new version of babel/preset-react - you actually upgraded this package... so, it means that when you later removed this - it removed not the "old version" of it, it removed it completely from your dependencies. So, you would need to remove the old version first and install the new one... ot just upgrade the package without removing it.

Anyway, I'm glad you got it working!

Cheers!

Reply
Alexander P. Avatar
Alexander P. Avatar Alexander P. | posted 3 years ago

This syntax is available by default since v7 of babel/preset-react

Reply

Great news! Thanks for sharing Alexander P.!

Reply
Cat in space

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

This course uses Symfony 4, but as this is a JavaScript course, all the concepts apply fine to Symfony 5. Have fun!

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