A product's Vue 3 migration: A real life story

Vue.js has just released the latest version of their JavaScript library called One Piece. We decided to dive in and update our product to use this new version and documented how our migration went.

In September 2020, Evan You, creator of the JavaScript library Vue.js, announced the stable release of Vue 3, labelled One Piece. Here at Suade Labs, we watched the announcement live and were very excited about the new features, including performance improvements, the Composition API, and the ability to teleport elements. These new features were exactly what we needed for our imminent projects and the Suade product, so we started getting to work and figuring out how to migrate our application. We could have waited for more dev resources like the Vue 2.6 migration build, but we wanted to start using these new features. So, we created a rough plan and got straight into it! This is how it worked out.

The Vue application

Our product’s Vue application has over 60k lines of Javascript code, supported by over 1500 end to end & unit tests. At Suade, we make it a priority to write good tests in our work, which as a result, highlights any issues we hadn't considered. Because of this, we can make expansive and dynamic changes to our codebase, without the fear of wondering if we have picked up all the use cases. This worked well with our Vue 3 migration, as we could pull things apart and be confident that the tests will pick up on anything unexpected. By having these tests, we could focus more on completing the upgrade, rather than worrying about “upsetting the apple cart”. We talk more about tests in another article called “Why I write tests.”.

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.

Getting started

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.

Updating Filters

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.

Updating props.sync

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 text.sync=”variable” to 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:

computed: { 
  internalText: { 
    get() { 
      return this.text; 
    }, 
    set(val) { 
      this.$emit(‘update:text’,val); 
    } 
  } 
} 

Then, in the component, we migrated all uses of this.text to 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.

Wrapping up

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:

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

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

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

So that’s how we migrated our product to Vue 3. We’ve been running Vue 3 on production for just over a month, and we haven’t had any major issues or show stoppers. We’re very happy that we can now use these new features like teleport to bring extra goodness to our clients. Shoutout to the Vue 3 contributors who have, and are still, creating a fantastic library that we love to use daily. If you want to join us in working with Vue 3 & JavaScript, and help create technology to prevent the next financial crisis, make sure you check out our job openings and see how you can make a difference in Regtech.

The Vue logo migration image is a derivative of the Vue, Macross, and One Piece logos by Evan You, used under CC BY-NC-SA 4.0. This image is licensed in the same way.

Written by Matt Elen.

Start a conversation

Subscribe to our Reg Round Up

Register your interest here

At Suade, we take your privacy and the protection of you personal data very seriously. You can read our website's Privacy Policy here to find out more about how we do this. By clicking 'I Accept' you agree to the terms of our Privacy Policy