Key Differences Between Vue 2 and Vue 3

Lifecycle Hooks

Vue 2 Description Vue 3
beforeCreate() Before creation setup()
created() After creation setup()
beforeMount() Before mounting onBeforeMount()
mounted() After mounting onMounted()
beforeUpdate() Before update onBeforeUpdate()
updated() After update onUpdated()
beforeDestroy() Before destruction onBeforeUnmount()
destroyed() After destruction onUnmounted()
errorCaptured() Error capture onErrorCaptured()

Priority of v-if and v-for

  • In Vue 2.x, v-for has higher priority than v-if.
  • In Vue 3.x, v-if has higher priority than v-for.

Ref Arrays in v-for

  • Vue 2.x automatically populates ref arrays.
  • Vue 3.x requires manual handling.
<ul>
    <li v-for='element in elements' :key='element.id' :ref='addRef'>
        {{ element.content }}
    </li>
</ul>
methods: {
    addRef(element) {
        this.refList.push(element);
    }
}

$children Property

  • Vue 2.x: Access child components via this.$children.
  • Vue 3.x: $children is removed; use $refs instead.
<child-component ref='childRef'/>
this.$refs.childRef

Composition API (setup)

Overview

The Composition API introduces setup() for organizing logic in components.

Reactivity System

  • Vue 2.x: Uses Object.defineProperty() for reactivity, which has limitations like inability to detect array changes directly and requiring property iteration.
  • Vue 3.x: Uses Proxy for reactivity, eliminating the need for iteration.

Data Definition

  • ref(): For primitive data types.
  • reactive(): For complex objects.

Auto-Import Plugin

Simplify imports with unplugin-auto-import.

npm install unplugin-auto-import --save-dev
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import AutoImport from 'unplugin-auto-import/vite';
import { resolve } from 'path';

export default defineConfig({
    plugins: [
        vue(),
        AutoImport({ imports: ['vue', 'vue-router'] })
    ],
    resolve: {
        alias: { '@': resolve(__dirname, './src') }
    }
});

toRefs Utility

Use toRefs() to destructure reactive objects while maintaining reactivity.

Computed Properties

const user = reactive({
    firstName: 'John',
    lastName: 'Doe',
    initials: computed(() => user.firstName.slice(0, 1) + user.lastName.slice(0, 1))
});

const message = ref('Hello');
const trimmedMessage = computed(() => message.value.trim());

const customComputed = computed({
    get() {
        return message.value.toUpperCase();
    },
    set(newValue) {
        console.log('Value set:', newValue);
    }
});

Watch Function

  • Vue 2.x:
watch: {
    targetObject: {
        handler(newValue, oldValue) {
            console.log(newValue, oldValue);
        },
        immediate: true,
        deep: true
    }
}
  • Vue 3.x:
// Watch a single reactive value
watch(dataValue, (newVal, oldVal) => {
    console.log(newVal, oldVal);
}, { immediate: true });

// Watch multiple values
watch([firstValue, secondValue], (newVals, oldVals) => {
    console.log(newVals, oldVals);
});

// Watch a nested property
watch(() => object.nested.array, (newVal, oldVal) => {
    console.log(newVal, oldVal);
});

// Immediate effect watcher
watchEffect(() => {
    console.log(dataValue.value);
});

Component Communication

Parent to Child

Parent:

<child-component :message='parentMessage'/>
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Data from parent');

Child:

<div>{{ message }}</div>
defineProps({
    message: {
        type: String,
        default: 'Default text'
    }
});

Child too Parent

Child:

<div>
    {{ count }}
    <button @click='updateCount'>Increment</button>
</div>
const count = ref(200);
const emit = defineEmits<{
    (e: 'updateCount', value: number): void
}>();

const updateCount = () => {
    emit('updateCount', count.value);
};

Parent:

<child-component @updateCount='handleUpdate'/>
import ChildComponent from './ChildComponent.vue';
const handleUpdate = (value) => {
    console.log(value);
};

v-model with Custom Props

Parent:

<child-component v-model:quantity='itemQuantity'/>
import ChildComponent from './ChildComponent.vue';
const itemQuantity = ref(1);

Child:

const props = defineProps({
    quantity: {
        type: Number,
        default: 100
    }
});
const emit = defineEmits(['update:quantity']);

const modifyQuantity = () => {
    emit('update:quantity', 200);
};

Sibling Communication with Event Bus

Install mitt:

npm install mitt --save

Event bus setup:

// bus.js
import mitt from 'mitt';
const eventBus = mitt();
export default eventBus;

Component A:

eventBus.emit('customEvent', data);

Component B:

eventBus.on('customEvent', (eventData) => {
    receivedData.value = eventData;
});

Lifecycle in Composition API

In the Composition API, lifecycle hooks are prefixed with 'on', such as onMounted(). Note that beforeCreate and created are not available in setup().

Routing

  • useRoute() replaces this.$route.
  • useRouter() replaces this.$router.

Slots

Default Slot

Parent:

<child-component>Slot content</child-component>

Child:

<div>
    <slot></slot>
</div>

Named Slots

Parent:

<child-component>
    <template v-slot:header>Header content</template>
    <template #footer>Footer content</template>
</child-component>

Child:

<div>
    <slot name='header'></slot>
    <slot name='footer'></slot>
</div>

Scoped Slots

Parent:

<template #default='{ item }'>
    {{ item.name }} - {{ item.age }}
</template>

Child:

<div v-for='element in list' :key='element.id'>
    <slot :data='element'></slot>
</div>

Dynamic Slots

Parent:

<template #[slotName]>Dynamic content</template>
const slotName = ref('header');

Teleport Component

Use <teleport> to render content in a different part of the DOM.

<teleport to='#target-container'>Content to teleport</teleport>

Dynamic Components

<component :is='currentComponent'></component>

Async Components

Lazy Loading

Load components only when needed.

<template>
    <div ref='observerTarget'>
        <AsyncComponent v-if='isVisible'/>
    </div>
</template>
import { useIntersectionObserver } from '@vueuse/core';
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
    import('./AsyncComponent.vue')
);

const observerTarget = ref(null);
const isVisible = ref(false);

const { stop } = useIntersectionObserver(
    observerTarget,
    ([{ isIntersecting }]) => {
        if (isIntersecting) {
            isVisible.value = true;
        }
    }
);

Suspense

Handle loading states with <Suspense>.

<Suspense>
    <template #default>
        <AsyncComponent/>
    </template>
    <template #fallback>
        Loading...
    </template>
</Suspense>
const AsyncComponent = defineAsyncComponent(() =>
    import('./AsyncComponent.vue')
);

Code Splitting

Async components are split in to separate JavaScript files during build.

Mixins

Composition API Mixin

// mixin.js
import { ref } from 'vue';
export default function useCounter() {
    const count = ref(1);
    const isActive = ref(false);

    const increment = () => {
        count.value += 1;
        isActive.value = true;
        setTimeout(() => {
            isActive.value = false;
        }, 2000);
    };

    return { count, isActive, increment };
}
// Component.vue
import useCounter from './mixin.js';
const { count, isActive, increment } = useCounter();

Options API Mixin

// mixin.js
export const counterMixin = {
    data() {
        return {
            count: 10
        };
    },
    methods: {
        addToCount(value) {
            this.count += value;
        }
    }
};
// Component.vue
import { counterMixin } from './mixin.js';
export default {
    mixins: [counterMixin],
    data() {
        return {
            message: 'Hello'
        };
    }
};

Provide and Inject

Provider:

provide('sharedData', reactiveData);

Injector:

const injectedData = inject('sharedData');

State Management

Vuex

  • Access state: computed(() => store.state.property).
  • Getters: computed(() => store.getters.getterName).
  • Mutations: store.commit('mutationName').
  • Actions: store.dispatch('actionName').
  • Modules: Similar to Vue 2.
  • Persistence with vuex-persistedstate:
npm install vuex-persistedstate --save
import createPersistedState from 'vuex-persistedstate';
export default createStore({
    modules: { user },
    plugins: [createPersistedState({
        key: 'app-storage',
        paths: ['user']
    })]
});

Pinia

  • Differences from Vuex: No mutations, modular by design, smaller size, direct state modification.
  • Usage: Define stores with defineStore().
  • Persistence: Use plugins like pinia-plugin-persistedstate.
  • Proxy configuration for CORS: Set up in build tools like Vite.

Tags: Vue.js Vue 2 Vue 3 Composition API Frontend Development

Posted on Sat, 16 May 2026 13:57:10 +0000 by awpti