Guides

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.

45-60 minutes per component5 steps
1

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.

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

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.

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

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().

MyComponent.vue
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.
4

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.

MyComponent.vue
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.
5

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.

MyComponent.vue
// 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.