A product's Vue 3 migration: A real life story
The Vue application
Not only do we have our product to upgrade, but we also have our own UI component library. This is filled with basic components like input boxes, but also more advance components such as data tables and graphs. Since our component library is built in house, we didn’t have many dependencies that needed their own Vue 3 upgrades. For those that did, we noticed that some authors were quick off the mark and updated their components and libraries to work with Vue 3. Others, we forked (hopefully will publish soon to our github account ) and upgraded them ourselves to Vue 3.
We started our Vue 3 migration by learning what has changed in Vue 3, using their very helpful migration guide. We figured out that the changes that would affect us the most are filters, event bus and the removal of prop.sync/multi v-models. Vue 3 no longer supports filters and using Vue as an event bus, so we decided to tackle these two changes first, as we didn’t need Vue 3 for migration of these uses. This also meant that we could publish these changes to our codebase whilst reducing the amount of work & changed code in the final Vue 3 commit.
Filters in Vue 2 allowed us to format and display strings and integers. As a financial regulatory software provider (also known as Regtech), being able to display monetary values in different currencies easily was one of the drivers for using filters throughout our products.
Our Vue 3 migration strategy around this was what Vue recommended - using the new global properties object and assigning the old filter functions in there. We then migrated each use of the filter to a global property function using several different regex patterns.
Removing Event Buses
Event buses were used in our product in non-consistent ways, so we couldn’t apply a blanket ‘apply all’ migration to them, like we could with the filters. So, we had to review each use and decide how we wanted to approach that particular use. In the end, most of the time we were using event buses to send data from one child component to another, so we replaced most of these with emitting events to parent components.
One of the advantages that event buses had was that an unspecified number of components could subscribe to events. This was so you didn’t need to know, when developing, how many components needed to receive an event. One scenario we had made use of that advantage: when a submit button in a form knew if a form was valid and could be submitted. Since every form has different components, it wasn’t as simple as others to migrate this usage of the event bus. Instead, we use the global properties feature of Vue 3 to feed in the components used in a form. Then the submit button can find out which components are in use from global properties, check if everything was valid and check if the form can be submitted.
Finally, our biggest Vue 3 migration issue was the removal of the props sync feature and migrating them to use a v-model prop. Thankfully, we found the Vue Next Plugin which converted code from
v-model:text=”variable”. This did a lot of the heavy lifting, but we also needed to change how components would interact with these props. Previously there was the ability to be able to change them directly, e.g.
this.text = “new value”, however we now needed to migrate this to emit an event of change to the parent element. So, we added a new computed property that could handle a set function:
Then, in the component, we migrated all uses of
this.internalText to handle when this prop was getting set. Fortunately, most of props.sync situations were handled by mixins, so while we were using props.sync extensively across the product, we only needed to tweak a small amount of mixins for it to work. We also relied on the Vue 3 ESLint Plugin and it’s Vue 3 essential rule set to inform us on where we were assigning data directly to props. Having this rule set also allowed us to auto fix a ton of issues, so we didn’t need to fix these manually. Before we started testing the product, we made sure that we had an error free code base to reduce the number of issues we would find.
But wait, there’s more
As we progressed through the Vue 3 migration, we realised that there were migrations that needed to be done of the Vue family of libraries (e.g. Vuex, Vue Router etc), not just Vue itself. Most of these migrations were very minor and straight forward, however the most complex one for us was with Vue Test Utils. The version that works with Vue 3 no longer allows you to mock methods on a component, so we had to come up with a new way for these situations.
In one case, a component would generate a random ID for setting an HTML ID attribute in the DOM. Since we were doing snapshot testing, we needed that ID to be the same between tests, so we had a mocked method. For these kinds of situations, we used a combo of Jest mocks, creating mixins with our functions/data that we specified needed for testing, and sometimes change the method in the Vue object before passing it to the Vue Test Utils’ mount method.
No more /deep/
Another change we did as part of the Vue 3 Migration was migrate our use of /deep/ to ::v-deep() with our CSS. Using these deep selectors allows us to create queries in scoped CSS that target child components and elements. This also wasn’t a simple ‘find and replace’, as v-deep requires an argument of a selector , but /deep/ did not.
We also had situations where we had /deep/ inside another /deep/. While I’m unsure if this was necessary at the time, we did not want to migrate them both to v-deep as when a v-deep was inside another v-deep, the second v-deep did not compile to anything useful and stayed as v-deep. This meant that the browser was left to deal with v-deep, which of course, did not know how to, and therefore ignored that selector query. This resulted in certain elements displaying incorrect styling. Once we fixed that up, we also used the Vue Scoped CSS ESLint Plugin to help detect where we might need to use v-deep and do manual checks and migration.
Overall, the Vue 3 migration went well. Since Vue 3 is relatively new, there hasn’t been a build-up of knowledge across the internet like in blog articles or answers to questions in Stack Overflow. Fortunately, Vue has a great Discord community to ask questions and see answers. This helped us to realise that they have dropped support for the @hook:mounted feature, after troubleshooting it for some time.
Through reviewing our Vue 3 migration, we have been able to understand what we would do differently next time. Here are our lessons learnt:
Keep up to date with minor versions of our dependencies. A lot of our Vue family dependencies were based on older versions, so when we were migrating to the latest version, we had to go through two or three sets of migrations for a single library. This resulted in a bigger, more complicated task. There’s the saying of “if it isn’t broken, don’t fix it”, which can be applied to not updating dependencies because everything still works by not touching it. Since we were a start-up, we previously would rather spend our time working on features rather than making more work for ourselves. But, to help improve our code base and product, we will keep updating our dependencies when they are updated.
Having great testing coverage helped us identify bugs and issues before manual reviews were completed. We knew when our migration was ready for a manual functional review when all our tests, both unit and e2e tests were passing. As a result, very few issues were picked up by our manual tests, and the issues that were picked up were from areas of our application where there were not many tests for.
Being more aware of our technical debt. Having technical debt is not an issue, but with any kind of debt, you must deal with it in the future. We had a piece of code that worked fine with Vue 2 however this code broke in Vue 3, and it wasn’t clear where the code issue was. The code was causing an infinite loop of Vue rendering, resulting in Vue stopping the render, warning us that this was happening, and freezing the application. Not ideal. After a couple of days going down rabbit holes and commenting code in and out of use, we discovered that in a component, a computed property was running a method, and in that method, it was changing data and props variables.
Using a computed function to change a data or prop is not recommended in Vue because it creates bad side-effects, or in our situation, an infinite render loop. Normally, Vue warns you that this is happening, but since the change was happening in a method, not a computed function, there was no such warning. We fixed this by changing how the code works and removed the function in question. Such a simple fix, considering how long it took to find the issue.
Written by Matt Elen.