Building Vue (general) with open-source tools
This guide provides a structured workflow for migrating legacy Vue 2 Options API components to the Vue 3 Composition API using <script setup>, TypeScript, and Pinia. It focuses on maintaining type safety and architectural consistency during the transition.
Initialize Script Setup and Define Component Interfaces
Replace the standard export default block with <script setup lang='ts'>. Define TypeScript interfaces for Props and Emits to replace the props and emits options. This enables better IDE intellisense and compile-time type checking.
interface Props {
title: string;
count?: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update', value: number): void;
(e: 'close'): void;
}>();⚠ Common Pitfalls
- •Do not use reactive() for props; they are already reactive and should be accessed via the props object to maintain reactivity loss prevention.
Migrate Data and Computed Properties
Convert the data() function into individual ref() or reactive() variables. Map computed properties to the computed() function. Use ref() for primitives and reactive() for complex objects where deep reactivity is required.
import { ref, computed } from 'vue';
const internalCount = ref(0);
const doubleCount = computed(() => internalCount.value * 2);
const state = reactive({
user: { name: 'Dev', role: 'Admin' },
settings: { theme: 'dark' }
});⚠ Common Pitfalls
- •Forgetting to use .value when accessing ref variables inside the script block.
- •Overusing reactive() which can lead to losing reactivity if destructuring occurs.
Transition Methods and Lifecycle Hooks
Convert Options API methods into standard JavaScript functions. Replace lifecycle hooks like mounted() and destroyed() with their Composition API equivalents: onMounted() and onUnmounted().
import { onMounted, onUnmounted } from 'vue';
function handleIncrement() {
internalCount.value++;
emit('update', internalCount.value);
}
onMounted(() => {
console.log('Component initialized');
});⚠ Common Pitfalls
- •The created() and beforeCreate() hooks do not have equivalents in Composition API; their logic should be placed directly in the setup body.
Inject Pinia Stores and Replace Vuex Calls
Replace this.$store access with Pinia store instances. Import the specific store hook (e.g., useUserStore) and initialize it at the top of the setup function. This allows for direct access to state, getters, and actions without the overhead of mapState or mapActions.
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { name, isAdmin } = storeToRefs(userStore);
function updateProfile() {
userStore.updateName('New Name');
}⚠ Common Pitfalls
- •Destructuring state directly from a Pinia store will break reactivity; use storeToRefs() to maintain the reactive connection.
Refactor Template Refs and V-Model
In Vue 3, template refs are handled by creating a ref with the same name as the 'ref' attribute in the template. Additionally, update v-model usage if the component uses multiple models or the default 'modelValue' prop name.
// In Script
const inputElement = ref<HTMLInputElement | null>(null);
// In Template
// <input ref="inputElement" type="text" />⚠ Common Pitfalls
- •Ensure the ref variable is initialized with null and typed correctly (e.g., HTMLInputElement) to avoid runtime errors before the component is mounted.
- •Vue 3 changed the default v-model prop from 'value' to 'modelValue'.
What you built
Successful migration to the Composition API improves code colocation and TypeScript integration. Once the component is refactored, consider extracting reusable logic into standalone composables in separate files to maximize code sharing across the application.