Migrating to Vue 2.7
Hello! In this article, I would like to share my experience of updating a project written in Vue 2.6. In addition to updating vue itself and components, I will show with examples how I managed to update other project dependencies and adapt them to work with composition APIamong them: Vuex, BootstrapVue, AgGrid And VueForm Generator.
History of Composition API in Vue
React
Oddly enough, we owe this innovation to React, or rather the concept of react-hooks introduced in 2018.
I have never touched React, just read its documentation briefly, so I can not speak objectively about this library feature, however, almost all React developers claim that hooks make it possible to greatly simplify development and create reusable code; Moreover, functional components using hooks are already the standard for React development.
Vue 3
With the release of vue 3rd version, a new approach to creating components became available to developers, similar to the functional components of react – composition API: method setup and <script setup> for use in single-file components.
Comparing the composition API with the options API, the following are usually listed as its advantages:
simplicity and conciseness
the ability to create reusable pieces of logic (instead of mixins)
improved TypeScrit support
bOGreater performance (according to the creators)
It is also worth considering critical assessments of this approach:
Sources
In general, it can be seen that all the shortcomings or concerns about using the composition API rest on the experience and knowledge of the developer: the composition API provides much more tools for working with reactivity, which naturally requires some effort to understand and be careful, especially if the developer is used to options or class API.
Vue 2.7
In July 2022, vue 2.7 was released, which added the composition API out of the box (previously this required the library @vue/composition-api), and added the ability to use . And despite the fact that vue 2.6 is no longer officially supported, and support for vue 2.7 will end in December 2023, libraries on vue 2, judging by the data from npm are still very often downloaded and used. It follows from this that the migration to vue 3 was not completely painless, and some libraries (for example bootstrap-vue) still Not ported to vue 3. It is also worth considering that vue 3 uses a reactivity system based on proxywhich Not supported by older browsers. Therefore, vue 2.7, in my opinion, is a relatively painless way to use the main feature of vue 3 - the Composition API, in your applications, without rewriting absolutely all the code and without switching to other libraries.
Motivation
In our company, the issue of switching to vue 3 has been raised more than once, however, the main library for our interfaces - bootstrap-vue still exists stably only for vue 2 version.BootstrapVue: @vue/compat
From version 2.23.0 Bootstrap-vue has a so-called migration build available. I tried to run a sample project on bootstrap-vue and @vue/compat from the developers themselves, and got a whole list of warnings from vue. This is one of the reasons why I didn't use bootstrap-vue in compat mode, another reason: migration build it is still needed for project migration, that is, the gradual replacement of its modules and rewriting to vue 3, but not for developing new products on it. After half a year, there is still no full-fledged release of bootstrap-vue for vue 3, but it is necessary to develop and maintain projects.Class API
To support typescript in vue 2, the vue development team created a library vue-class-component. And using the library vue-property-decoratoryou can declare props, refs and emits in decorator style and type them. However, the Class API has a number of significant drawbacks, which written by Evan Yu:- It doesn't achieve its main purpose (better TypeScript support)
- Complicates internal implementation
- Doesn't improve logical composition
Typing
Also, in order to achieve typing, you have to write Very a lot of extra code, here is an example of a store using VuexSmartModule.toasts_store.ts
// ...
class ToastState {
count = 0
toasts: Toast[] = []
}
class ToastGetters extends Getters<ToastState> {
get toasts() {
return this.state.toasts
}
get count() {
return this.state.count
}
}
class ToastMutations extends Mutations<ToastState> {
pushToast(toast: Toast) {
this.state.toasts.push(toast)
}
spliceToast(id: number) {
this.state.toasts = this.state.toasts.filter(p => p.id !== id)
}
incCount() {
this.state.count++
}
}
class ToastActions extends Actions<
ToastState,
ToastGetters,
ToastMutations,
ToastActions
> {
async pushToast(toast: Toast) {
toast.id = this.state.count
this.mutations.incCount()
this.mutations.pushToast(toast)
}
async delToast(id: number) {
this.mutations.spliceToast(id)
}
}
export const toast = new Module({
state: ToastState,
getters: ToastGetters,
mutations: ToastMutations,
actions: ToastActions
})
export const toastMapper = createMapper(toast)
Complexity of implementation
In vue-class-component there is another trap this at the stage of class creation it is Not same thisas in the component, all because our class is first converted from a class to an object understandable for vue and only after that the component is created, you can check it like this:<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
@Component({})
export default class Test extends Vue {
self: Test | null = null
constructor() {
super()
this.self = this
console.log('constructor:', this.self == this)
}
created() {
console.log('created:', this == this.self)
}
}
</script>
Get the following picture:constructor: true
created: false
This also applies to functions in objects declared in a class, you just don't have access to the actual state of the object (this in this case is still that object derived from the class).
// ...
obj = {
f: () => {
// this != контекст компонента
}
}
// ...
Therefore, in such cases, it was necessary to create a separate method in the class and pass it already, even if it was required just to change one field in the object, or to emit something.
// ...
private schema: FormSchema<SelectData> = {
fields: [
{
// ...
onChanged: this.onSelected
},
{
// ...
onChanged: this.onSelected
}
]
}
// ...
private async onSelected() {
// Какая-то логика
}
// ...
However, before vue 2.7, it was possible to access props and stores inside objects (if you mapped them to methods).
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
const Mappers = Vue.extend({
methods: {
...mobileWidthMapper.mapGetters(['isMobile'])
}
})
@Component({})
export default class Test extends Mappers {
@Prop({ required: true }) a!: number
@Prop({ required: true }) b!: string
obj = {
a: this.a,
b: this.b
c: this.isMobile()
}
created() {
console.log(this.obj) // получим корректные значения
}
}
</script>
Logical composition
Before the advent of the composition API, mixins were the only way to create reusable component logic, they have significant drawbacks:- Namespace intersection
- Complexity of typing
- Opacity of requirements (for example, if the mixin requires the presence of certain props or methods on the component)
- Difficulty in understanding and debugging: when you access all methods and objects through thisit can be difficult to understand where it came from: from the object itself, from the store, or from some kind of mixin.
Steps for migration
vue update
First of all, I updated vue itself to version 2.7, and immediately some of the components that used props when declaring internal objects stopped working for me, as in the example above. I decided to immediately rewrite such components on the composition API, but you can also try to take the necessary props in the life cycle hooks.Vuex / Pinia
Next in line is the repository, in general, there were no particular difficulties here, I connected pinia and gradually rewrote all the vuex modules. If you also used VuexSmartModule and don't want to rewrite all components, then pinia stores can also be mapped like this:const Mappers = Vue.extend({
computed: {
// Либо mapState/mapActions из pinia
...mapStores(useUserStore /*, и другие*/)
}
})
@Component
export default class SomeComponent extends Mappers {
// ...
}
For comparison, I will show the above-described store for toasts, now on pinia.
toasts_store.ts
...
export const useToastStore = defineStore('toast', () => {
const count = ref(0),
toasts = ref<ToastInner[]>([])
function pushToast(toast: Toast) {
toasts.value.push({ ...toast, id: count.value++ })
}
function delToast(id: number) {
toasts.value = toasts.value.filter((p) => p.id !== id)
}
return {
count,
toasts,
pushToast,
delToast
}
})
store.dispatch('user/fetchCurrentUser').then(() => {
new Vue({
router,
store,
// прочие плагины
render: h => h(App)
}).$mount('#app')
})
That is, before the start of the application, it was required to obtain user data in order to use them in the router.
Perhaps it would be more correct to put the user load in the middleware of the router, but it turned out that we have a lot where storage data is also used in redirect router hooks that Not can be asynchronous, so it will not be possible to load data there.
I got out of it like this: before creating the main Vue instance with the root application component, I created an empty instance with only one pinia.
// Всё это внутри асинхронной функции
const pinia = createPinia()
// Фиктивный инстанс vue для инициализации хранилища
const piniaLoadApp = new Vue({ pinia })
await useUserStore().fetchCurrentUser()
new Vue({
pinia,
router,
i18n,
render: (h) => h(App)
}).$mount('#app')
piniaLoadApp.$destroy()
Accordingly, after that we will get access to all the stores.
BootstrapVue
In general, work with this library has not changed, but in order to access the $bvToast and $bvModal objects in i created a simple composable// ...
export function useBVModal() {
return getCurrentInstance()?.proxy.$bvModal
}
export function useBVToast() {
return getCurrentInstance()?.proxy.$bvToast
}
AgGrid
AgGrid it is very powerful and one of the most popular spreadsheet libraries, it is widely used in our projects. After updating it, I encountered some deprecated warnings. However, if you follow what is written in them, then you can update everything correctly without any special changes, basically these are just different names for the fields in the settings objects. But I found one problem, most likely a bug in the library itself (at the time of this writing, it is still not fixed, but I have already submitted an issue). If you declare a column with your own renderer (component) in column-defs, like this:import CustomRenderer from '.../CustomRenderer.vue'
// ...
const colDefs: ColDef[] = [
// ...
{
// ...
cellRenderer: CustomRenderer,
}
// ...
]
Most likely you will have something like this
At the same time, it's funny, but if you write instead of cellRenderer
- cellRendererFramework
then you will get deprecated warning from the library, but everything will work.
You could try passing in cellRenderer
string instead of a component, but this will also fail, since in <script lang="ts">
import CustomRenderer from '.../CustomRenderer.vue'
import { defineComponent } from 'vue'
export default defineComponent({
// eslint-disable-next-line vue/no-unused-components
components: { CustomRenderer }
})
</script>
<script setup lang="ts">
// ...
const colDefs: ColDef[] = [
// ...
{
// ...
cellRenderer: 'CustomRenderer',
}
// ...
]
</script>
VueFormGenerator
VueFormGenerator - a small library for generating forms using JSON schemas, it is no longer supported by developers and in general I don’t really like it, but it has not yet been thrown out of projects, since we have made all forms on it.
To adapt it to the composition API, we had to make a cumbersome composable based on their mixin abstractFieldand to reuse props and emits, I made objects passed to defineProps And defineEmits. There is a lot of code, so I'll just leave link to gist.
The simplest custom field (regular label) using the given composable:
<template>
<span>
{{ value }}
</span>
</template>
<script setup lang="ts">
import { useField } from ".../use-field";
import { FieldPropsObject, FieldEmitsObject, FieldExpose } from ".../types";
const props = defineProps(FieldPropsObject);
const emit = defineEmits(FieldEmitsObject);
const { clearValidationErrors, validate, value } = useField(
props,
emit
);
defineExpose<FieldExpose>({ validate, clearValidationErrors });
</script>
What have we achieved
Despite all the difficulties that had to be overcome, at the moment my opinion is unequivocal: it was worth it, we got a much more compact, understandable, more maintainable code, while not losing typing.
A special nice bonus was the ability to use the library VueUsewith the help of which, for example, it was possible to replace the store for the window size and the mixin for tracking changes in its size, with just one function useWindowSize.
You can read a little more about the migration process itself in this article.