Options API to Composition API Migration

+15 Mana ✨

Learn to convert existing Options API components to the Composition API.

Side-by-Side Comparison

Options API

vue
<script>
export default {
  props: {
    userId: {
      type: Number,
      required: true
    }
  },
  
  emits: ['update', 'delete'],
  
  data() {
    return {
      user: null,
      loading: false,
      error: null
    }
  },
  
  computed: {
    fullName() {
      return this.user ? `${this.user.firstName} ${this.user.lastName}` : ''
    },
    isAdmin() {
      return this.user?.role === 'admin'
    }
  },
  
  watch: {
    userId: {
      immediate: true,
      handler(newId) {
        this.fetchUser(newId)
      }
    }
  },
  
  methods: {
    async fetchUser(id) {
      this.loading = true
      this.error = null
      try {
        const response = await fetch(`/api/users/${id}`)
        this.user = await response.json()
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },
    handleUpdate() {
      this.$emit('update', this.user)
    },
    handleDelete() {
      this.$emit('delete', this.userId)
    }
  },
  
  mounted() {
    console.log('Component mounted')
  }
}
</script>

Composition API Equivalent

vue
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

// Props
const props = defineProps<{
  userId: number
}>()

// Emits
const emit = defineEmits<{
  update: [user: User]
  delete: [userId: number]
}>()

// Data (reactive state)
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)

// Computed
const fullName = computed(() =>
  user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
)
const isAdmin = computed(() => user.value?.role === 'admin')

// Methods
async function fetchUser(id: number) {
  loading.value = true
  error.value = null
  try {
    const response = await fetch(`/api/users/${id}`)
    user.value = await response.json()
  } catch (e) {
    error.value = (e as Error).message
  } finally {
    loading.value = false
  }
}

function handleUpdate() {
  if (user.value) {
    emit('update', user.value)
  }
}

function handleDelete() {
  emit('delete', props.userId)
}

// Watch
watch(
  () => props.userId,
  (newId) => fetchUser(newId),
  { immediate: true }
)

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

Migration Mapping

Options APIComposition API
props: {}defineProps<{}>()
emits: []defineEmits<{}>()
data()ref() / reactive()
computed: {}computed()
watch: {}watch()
methods: {}Regular functions
mounted()onMounted()
this.xxxDirect access / .value
this.$emit()emit()

Extracting to Composables

During migration, extract reusable logic:

typescript
// composables/useUser.ts
export function useUser(userId: Ref<number>) {
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  async function fetchUser() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(`/api/users/${userId.value}`)
      user.value = await response.json()
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }
  
  watch(userId, fetchUser, { immediate: true })
  
  const fullName = computed(() =>
    user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
  )
  
  return { user, loading, error, fullName, refetch: fetchUser }
}

Component becomes much simpler:

vue
<script setup lang="ts">
import { useUser } from '@/composables/useUser'

const props = defineProps<{ userId: number }>()
const emit = defineEmits<{
  update: [user: User]
  delete: [userId: number]
}>()

const { user, loading, error, fullName } = useUser(toRef(props, 'userId'))

function handleUpdate() {
  if (user.value) emit('update', user.value)
}
</script>
✓ Completed