Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Losing Reactivity

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

Vue reactivity is magic. What I mean by "reactivity" is how Vue is smart enough to re-render whenever a piece of data changes. Or, even more impressive, if that piece of data is an object and you change just one property on it, Vue will still figure out that it needs to re-render any components that depend on that.

The Situations When Reactivity Fails

But there are a few limitations to Vue reactivity: a few edge cases where Vue can't work its magic and does not realize that it needs to re-render. Well, to be clear: Vue 2 has a few limitations... that Vue 3 solves. So if you're using Vue 3, feel free to skip ahead: it does not suffer from this issue.

Search for "Vue reactivity" to find a page on their docs. Scroll down to "change detection caveats".

There aren't many situations like this, but this first situation talks about what is currently happening to us: Vue cannot detect property addition or deletion. And a property must be present in the data object in order for Vue to convert it and make it reactive.

We talked about how reactivity works under the hood in part one of this series. The short explanation for Vue 2 is that when a piece of data is an object, Vue replaces each property on that object with a getter and setter method. This is invisible to us, but it allows Vue to be notified - via the setter method - whenever someone changes a property.

Our Reactivity Problem: Property Addition

Our problem starts in the checkout form component's data function. We initialize validationErrors to an empty object. And then, in the validateField() method, we add a new property to validationErrors:

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 95
data() {
return {
... lines 98 - 105
validationErrors: {},
... lines 107 - 108
};
},
methods: {
... lines 112 - 153
validateField(event) {
... lines 155 - 165
if (!this.form[validationField]) {
this.validationErrors[validationField] = validationMessages[validationField];
} else {
delete this.validationErrors[validationField];
}
},
},
};
</script>

That's the "property addition" that Vue was talking about. Vue doesn't have a way to detect that the new property was added. And so, it can't add the getter and setter methods that are the key to making that property reactive. We can still read from and write to that property... but Vue isn't aware that we're doing that.

This is a long way of saying that if you have a piece of data that's an object like validationErrors, be sure to include all of its properties when you initialize it, even if some are null.

Head up to data and add all 6 properties to the object - setting each one to null. Thanks to this, from the very first moment the data is initialized, it will have all of its properties:

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 95
data() {
return {
... lines 98 - 105
validationErrors: {
customerName: null,
customerEmail: null,
customerAddress: null,
customerZip: null,
customerCity: null,
customerPhone: null,
},
... lines 114 - 115
};
},
... lines 118 - 179
};
</script>

Then, we're not creating a property down inside validateField(): we're just changing its value!

Oh, and now, instead of deleting the property, set it to null.

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 117
methods: {
... lines 119 - 160
validateField(event) {
... lines 162 - 172
if (!this.form[validationField]) {
this.validationErrors[validationField] = validationMessages[validationField];
} else {
this.validationErrors[validationField] = null;
}
},
},
};
</script>

Ok! Let's test this! Go to checkout and... perfect! It instantly updates! But if we submit the form... funny things start to happen.

No validation error on Ryan. Right? That makes sense. But if I clear that out and hit tab... hey! Why didn't I get my validation error? This... is the same problem, but I want to show it to you in more detail.

Back in the component, at the top of validateField(), let's console.log(this.validationErrors).

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 117
methods: {
... lines 119 - 160
validateField(event) {
console.log(this.validationErrors);
... lines 163 - 179
},
},
};
</script>

Head back over: my page already refreshed. Go to checkout and open the console. Now notice: when I first blur, the entire object has a .... That's because the validationErrors data is wrapped in a getter method, which is Vue's way of adding reactivity to it. And if we click to open this, each property also has a ... next to it. That's an easy way for us to see that each property is reactive: Vue did have the opportunity to wrap it in a getter and setter.

Now submit the form, focus the name field... and hit tab again. Scroll down on the console to see the new log. The object does not have the ... anymore. And more importantly, each property under it also does not have .... The fact that those are gone means that each property lost reactivity. If we set the customerCity property, there is no setter, and so Vue would not be notified that it needs to re-render.

The reason this is happening is, up at the top of onSubmit(), we're resetting validationErrors back to an empty object. Then, when we set a key on validationErrors later, we are, once again, creating new properties.

Let's reinitialize just one field to start: set customerName to null.

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 117
methods: {
... lines 119 - 132
async onSubmit() {
... lines 134 - 135
this.validationErrors = {
customerName: null
};
... lines 139 - 161
},
... lines 163 - 182
},
};
</script>

Now go back, head to the checkout form and re-submit it. Click on the name field and blur it to get the log. Oooo: customerName now does still have its getter method! But the other fields do not. By including the customerName property when we replaced the validationErrors data, Vue was able to wrap it and make it reactive at that moment.

So the full solution is this. Either use Vue 3... and this will all just work, or whenever you set a full key on data that's an object, whether you're setting it inside the data function or somewhere else - be sure to include every property it needs. There are other work arounds the docs mention, but this is what I like.

To do this without repeating ourselves, let's add a new method called getEmptyValidationErrors() that will return an object. Go up to our initial data, steal those fields, head down and paste. Perfect.

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 110
methods: {
... lines 112 - 173
getEmptyValidationErrors() {
return {
customerName: null,
customerEmail: null,
customerAddress: null,
customerZip: null,
customerCity: null,
customerPhone: null,
};
},
},
};
</script>

We can use this up inside data(): validationErrors set to this.getEmptyValidationErrors()

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 95
data() {
return {
... lines 98 - 105
validationErrors: this.getEmptyValidationErrors(),
... lines 107 - 108
};
},
methods: {
... lines 112 - 173
getEmptyValidationErrors() {
return {
customerName: null,
customerEmail: null,
customerAddress: null,
customerZip: null,
customerCity: null,
customerPhone: null,
};
},
},
};
</script>

Do the same thing down here in onSubmit(): this.getEmptyValidationErrors().

... lines 1 - 77
<script>
... lines 79 - 83
export default {
name: 'CheckoutForm',
... lines 86 - 95
data() {
return {
... lines 98 - 105
validationErrors: this.getEmptyValidationErrors(),
... lines 107 - 108
};
},
methods: {
... lines 112 - 125
async onSubmit() {
... lines 127 - 128
this.validationErrors = this.getEmptyValidationErrors();
... lines 130 - 152
},
... lines 154 - 173
getEmptyValidationErrors() {
return {
customerName: null,
customerEmail: null,
customerAddress: null,
customerZip: null,
customerCity: null,
customerPhone: null,
};
},
},
};
</script>

Let's check it! Go back to the checkout form, submit it... see the errors, type a name, hit tab and... it's gone! Reactivity is back!

Let's celebrate by removing the console.log().

Woh team, we're done! You did it! You are now massively dangerous in Vue. So go build something really cool and tell us about it. I would love to know.

In a future tutorial, we'll cover the Vue 3 composition API: that's the really big new, optional feature in Vue 3 that has the potential to make sharing code and data a lot nicer.

If there's something else that you want to know about, let us know down in the comments.

Alright friends, see you next time!

Leave a comment!

10
Login or Register to join the conversation
Chris N. Avatar

I finished the tutorial but didn't get a certificate and it shows as 99% complete on my profile :(

Reply

Hey Chris,

Look closer at the table of content on the course intro page: https://symfonycasts.com/sc... - every challenges should be green and you should see a little tick in front of each chapter. I bet there's a missing challenge or chapter there, you just need to complete the missing challenge or watch the chapter till the end.

If you still need help with this - write to us using contact form here: https://symfonycasts.com/co... from your account email and I'll take a look at it myself.

Cheers!

Reply
Chris N. Avatar

Thanks Victor
You are right, one of the chapters did not have a tick. This reminds me of something I noticed during the course, it would replay chapters I had already watched and I would have to redo the challenge/s that followed. It would step back one or two chapters (I think back to the chapter that followed any previous challenge).
I use the Brave browser so I wondered if it was struggling to keep my state? I used the same computer each time.
Thanks again
Chris
(Ah, looks like Disqus has also forgotten me - the burden of just wanting to not be quite so tracked)

Reply

Hey Chris,

Ah, if you have a tricky browser - it might be the reason why it missed the check. If somehow it was log you out globally on SymfonyCasts but the page still was loaded - you were able to watch the video I suppose, but the system didn't get a request about finishing it because you were not logged in :) Anyway, the fix should be pretty easy - just re-watch that chapter (or cheat a bit and just rewind it to the end :) )

Let me know if that didn't help ;)

Cheers!

Reply

Great couple of tutorials. Good job fixing the vimeo streaming issue btw! The only thing stopping me from migrating my existing app to Vue is that I don't know how to integrate the symfony translation bundles. I am using lexik/translation-bundle. Should I just add it to the twig template as usual and then use it within js? Do you recommend using another bundle?

Reply

Hey Manuel!

I don't have advise at this time as per a specific bundle to deal with i18n in Vue. When it comes to Vue and just Vue, there is a package called (Vue-i18n) which can help but might not adapt to your current method of doing it.

If you are using a Symfony bundle to do translations (and it works for you) my advise would be... Don't throw it away! You can dump your translation strings all in the template inside a Javascript object. These will likely be cached by twig anyway and you can then use these strings inside your Vue App. For example:


window.myTranslations = {
trans_string_a: 'put twig escaped string for js here',
trans_string_b: 'put your other strings ...',
};

Then you can easily reference the global variable.

Hope this helps!

Reply

Great tutorial!

I would really like to see one with a Vue SPA.

Reply

Hey Julien!

Thanks for your feedback! While a Vue SPA tutorial (featuring Vue Router) is inot in our immediate plans, we are considering adding this to our JS Frameworks track at some point. Stay tuned!

Reply
Intexsys I. Avatar
Intexsys I. Avatar Intexsys I. | posted 1 year ago

Thanks a lot for tutorials!
Really want to get know about how to combine Vue with Symfony Forms (maybe without API Platform, but with plain action with formView from controller) in proper way, and maybe how to compile Twig markup to Vue, so for example if i already have form-blocks in twig (form_widget, form_row, etc..) how it's possible to use them with Vue.

Reply

Hi Kirill!

In my view, Symfony Forms and Vue go separate ways. When it comes to forms specifically, if you use one system, you won't be using the other as their purpose are the same!

Reply
Cat in space

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

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.3
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.2.0
        "eslint-plugin-import": "^2.19.1", // 2.22.1
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.12
        "vue-loader": "^15.9.1", // 15.9.4
        "vue-template-compiler": "^2.6.11", // 2.6.12
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}