To use this site please enable javascript on your browser! Vue Composition API vs Options API

Vue Composition API vs Options API

by Bryce Andy 12:05 May 10 '22

When migrating from Vue 2 to version 3, developers might be tempted to using the composition API especially for large scale or enterprise applications.

In this article, we are going to explore how the composition API differs to the option API and how to transition to Vue 3 using the composition API.

Vue Composition API vs Options API

Why the Composition API?

Coming from Vue 2, it is important to ask yourself do you really need to transition to using the new compositon API in Vue 3?

To answer that, you  should know what the composition API is trying to solve.

If your components seem to keep growing and have the component logic interchanged all around, such that you can't keep track of individual logic of the component, then you need the composition API.

How to Use the Vue Composition API

Below are two code samples. The first uses the usual options API and the other is the same implementation while using the composition API:

<script>
  export default {
    data () {
      return {
        name: 'Andrew'
      }
    },
    methods: {
      greet () {
        console.log(`Hello ${this.name}`)
      }
    },
    created () {
      this.greet()
    }
  }
</script>
<script setup>
  const name = ref('Andrew')
  const greet = () => console.log(`Hello ${name.value}`)
  onMounted (greet)
</script>

Not only have we been able to use 12 less lines of code, but also you can see how the composition API can be easily read and maintained once this codebase grows.

Composition API setup Hook

We have a unique setup hook which serves as an entry point in the usage of the composition API. It is written in the following formats:

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // expose to template and other options API hooks
    return {
      count
    }
  },

  // Other hooks of the options API may follow here
}
</script>

Or the shorthand and the recommended way:

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

Within the setup hook you can't have access to the component instance, so when calling this, it will always return undefined.

NOTE: The shorthand may only be used when dealing with single page components.

Our discussion will therefore only cover SFC as they have many more advantages over other options.

Composition API Reactivity with ref & reactive

You may notice the use of ref and reactive to declare data properties. This is the composition API way of making properties reactive.

The main differences you should be aware of are:

  • ref can be used on primitive data types like String, Boolean, Number - while reactive is better used with non-primitive data types like Array, Object
  • ref receives a value and returns a reactive mutable object, while reactive returns a reactive proxy of the object
  • To access the data created with ref outside of the template you need to call the .value property of the object, unlike the one with reactive
<script setup>
import { reactive, ref } from 'vue'

const count = ref(10)
const user = reactive({
  name: 'Antoine Griezmann',
  age: 29
})

count.value++
console.log(count.value) // 11

user.age = 35
console.log(user) // { name: 'Antoine Griezmann', age: 35 }
</script>

Composition API Props with Reactivity

If a component needs to declare props, you may use the defineProps macro:

<script setup>
import { defineProps } from 'vue'

defineProps({
  title: String,
  likes: Number
})
</script>

In order to make a prop reactive so it can be used elsewhere with another file like a composable, we can make use of the toRef and toRefs utilities:

<script setup>
import { defineProps, toRef } from 'vue'

const props = defineProps({
  title: String,
  likes: Number
})

// Use toRef for primitives
const titleRef = toRef(props, 'title')

// ...then you can use the prop with another file, such as a composable
useSomething(titleRef)
</script>

This way the reactivity of the prop will be synced.

Composition API Reactivity with Computed Properties

You may wonder how would you declare computed properties using the composition API. Say we have a property fullname, all we have to do is wrap a computed hook with a callback inside it:

<script setup>
import { computed, reactive } from 'vue'

const user = reactive({
  firstname: 'Jane',
  lastname: 'Doe'
})
const fullname = computed(() => `${user.firstname} ${user.lastname}`)
console.log(fullname) // 'Jane Doe'
</script>

The example shown above is for readonly computed properties. For writable computed properties, we can pass the getter and setter functions:

<script setup>
import { computed, reactive } from 'vue'

const user = reactive({
  firstname: 'Jane',
  lastname: 'Doe'
})
const fullname = computed({
  get: () => `${user.firstname} ${user.lastname}`,
  set: (name) => {
    [user.firstname, user.lastname] = name.split(' ')
  }
})
</script>

Composition API Reactivity Using Watchers

The way we create watchers in the composition API is also slightly different from the options API.

This time you can make use of two hooks, watch and watchEffect:

<script setup>
import { ref, watch, watchEffect } from 'vue'

const name = ref('Jane Doe')
const age = ref(28)

// The callback is called whenever `name` changes
watch(name, () => {
  console.log(name.value)
  console.log(age.value)
})

// The callback is called immediately, and whenever `name` or `age` changes
watchEffect(() => {
  console.log(name.value)
  console.log(age.value)
})
</script>

You may add an optional options object argument after the watchEffect callback, see more here.

Suppose you want to access the old and new values from the watcher, this is only possible with the watch hook:

<script setup>
import { reactive, watch } from 'vue'

const user = reactive({ age: 23 })

watch(user, (newUser, oldUser) => {
  // triggers on deep mutation to state
})
</script>

Just like the watchEffect, watch has it's own options object for the third argument. The same options you used in the options API.

Composition API Lifecycle Hooks

All lifecycle hooks can be called within setup, but in the composition API they begin with an on prefix and receive a callback:

<script setup>
import { onBeforeUpdate, onMounted } from 'vue'

onMounted(() => {}) // Do sth here when the component is mounted
onBeforeUpdate(() => {}) // Do sth here before the DOM tree is updated due to a reactive state change
</script>

Composition API Dependency Injection

Depenency injection using the composition API is relatively straight forward as you are only required to call the provide and inject hooks.

// Parent component
<script setup>
import { provide, reactive } from 'vue'

const user = reactive({ name: 'Joe Doe' })

provide('user', user)
</script>

Then you may use the dependency in a deeply nested child:

// Child component
<script setup>
import { inject } from 'vue'

const user = inject('user')
</script>

Composition API with Composables

Using composables will enable us to clean up most of our code written in the options API to a neater looking version of the composition API as illustrated in the image above 👆.

The type of components we are deling with are usually large, difficult to read and as a result very difficult to maintain.

Consider we have a search component written in the options API that's going to search users of a bank, and this component has 3 features:

  • A functionality to list data
  • Another one that searches the users
  • And one that filters the user data

With just 3 features you can imagine how the component's logical concerns will be filled threefold in all the options (data, components, props, computed, watch, methods, and lifecycle hooks):

<script>
export default {
  components: [
    // All components related to listing, searching and filtering
  ],
  props: {
    // All props of this component that will support listing, searching and filtering
    bankId: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      // All data related to listing, searching and filtering
      users: [],
      filters: [],
      searchQuery: ''
    }
  },
  computed: {
    // All computed properties related to listing, searching and filtering
    usersMatchingSearchQuery () {
      // ...
    },
    filteredUsers () {
      // ...
    }
  },
  watch: {
    // All watchers of this component that will support listing, searching and filtering
    bankId: 'getBankUsers'
  },
  methods: {
    // All methods related to listing, searching and filtering
    getBankUsers () {
      // ... Fetch users of this.bankId
    },
    updateFilters () {
      // this.filters = 
    }
  },
  // All the lifecycle hooks that will support listing, searching and filtering...😑
  mounted () {
    this.getBankUsers()
  }
}
</script>

You can already tell it will be a tough ask to maintain this single component even though we have not included everything in its skeleton.

So, how can we group these 3 logical concerns each in their dedicated section so our component can be readable and maintainable?

This is where composables comes in. What is a composable?

A function that leverages the composition API to encapsulate and reuse stateful logic.

So we can see that using composables will help us encapsulate our logical concerns each to their own function and make them reusable in the main component:

// Search.vue
<script setup>
import useListUsers from '@/composables/useListUsers'
import useSearchUsers from '@/composables/useSearchUsers'
import useFilterUsers from '@/composables/useFilterUsers'

</script>

// ./src/composables/useListUsers.js
export default function useListUsers () {
}

// ./src/composables/useSearchUsers.js
export default function useSearchUsers () {
}

// ./src/composables/useFilterUsers.js
export default function useFilterUsers () {
}

Quickly you may notice we are trying to abstract each logical concern to its own composable file. Then later we can use them in the main search component.

For the purpose of this article we are going to implement two logical concerns.

Let's begin with the listing functionality, everything concerned with listing users will go to the listing composable:

// ./src/composables/useListUsers.js

import { fetchBankUsers } from '@/api/users'
import { onMounted, reactive, watch } from 'vue'

export default function useListUsers (bankId) {
  const users = reactive([])
  const getBankUsers = async () => {
    users = await fetchBankUsers(bankId.value)
  }

  onMounted(getBankUsers)
  watch(bankId, () => {
    users.splice(0, users.length)
    getBankUsers
  })

  return { users }
}

In order to manage all listing functionalities, let's describe what this composable has done:

  • The composable receives a bankId argument, which it will use to list bank users through a supposed API call on the getBankUsers method.
  • Since this composable is responsible with listing users, it will be the one to declare the reactive users property, which is an empty array.
  • Since in the options API we list users when the component is mounted, we have now done the same using the onMounted hook.
  • Also we have implemented the watcher from the options API which lists new users once the bankId changes.
  • Finally, it exposes the users list to the composable fuction so that it can be used in the search component.

Going back to the search component, let's declare the bankId prop and pass it to the useListUsers composable:

<script setup>
import { defineProps, toRef } from 'vue'
import useListUsers from '@/composables/useListUsers'

const props = defineProps({ bankId: String })

// Converting props.bankId to a ref using toRef and passing it 
// to the composable will make the prop reactive and
// enable the watcher to function properly
const { users } = useListUsers(toRef(props, 'bankId'))
</script>

We have completed the listing of users with two lines of code. Users will be listed when component is mounted and the watcher will update the list once the bankId changes.

Now let's move on to the searching:

// ./src/composables/useSearchUsers.js

import { computed, ref } from 'vue'

export default function useSearchUsers (users) {
  const searchQuery = ref('')
  const usersMatchingSearchQuery = computed(
    () => users.filter(user => user.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
  )

  return { searchQuery, usersMatchingSearchQuery }
}

This composable function has accomplished the following in order to enable searching of users:

  • Since it is responsible with searching, it has declared a reactive string searchQuery
  • Then using the computed hook, the search results are stored in a computed property.

Note that only searchQuery will be accessed with .value since it was declared with ref, unlike users

  •  Finally, the data property and the computed property are returned so they could be exposed by the composable later.

Back to the search component, we may fetch the two properties from the composable:

<script setup>
import { defineProps, toRef } from 'vue'
import useListUsers from '@/composables/useListUsers'
import useSearchUsers from '@/composables/useSearchUsers'

const props = defineProps({ bankId: String })
const { users } = useListUsers(toRef(props, 'bankId'))
const { searchQuery, usersMatchingSearchQuery } = useSearchUsers(users)
</script>

You may notice that we are not users reactive since it's already reactive.

And just like that we have the search complete. All that's missing is to use these properties in the template. Here is a sample that is styled with Tailwindcss 3:

<template>
  <div class="relative max-w-[320px]">
    <svg id="search-icon" class="w-5 h-5 fill-current absolute transform translate-y-3/4 translate-x-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M30 28.59 22.45 21A11 11 0 1 0 21 22.45L28.59 30ZM5 14a9 9 0 1 1 9 9 9 9 0 0 1-9-9Z" /><path fill="none" d="M0 0h32v32H0z" /></svg>
    <input
      class="rounded-full pl-8 pr-6 py-3 border focus:outline-none focus:ring-2 focus:ring-offset-2
      focus:ring-emerald-200 w-full"
      placeholder="Search bank users..."
      v-model="searchQuery"
    />
    <div
      id="users-list"
      class="py-2 flex flex-col absolute top-[110%] left-0 h-auto w-full z-10 rounded-lg bg-green-50"
      v-show="displayUsersList()"
    >
      <a
        href="#"
        class="block hover:bg-emerald-100 py-2 px-4 text-left"
        v-for="{ id, name } in usersToDisplay()"
        :key="id"
      >
        {{ name }}
      </a>
    </div>
  </div>
</template>

<script setup>
import { defineProps, toRef } from 'vue'
import useListUsers from '@/composables/useListUsers'
import useSearchUsers from '@/composables/useSearchUsers'

const props = defineProps({ bankId: String })
const { users } = useListUsers(toRef(props, 'bankId'))
const { searchQuery, usersMatchingSearchQuery } = useSearchUsers(users)

// Additional methods to cleanup the template
const displayUsersList = () => users.length > 0 || usersMatchingSearchQuery.value.length > 0
const usersToDisplay = () => usersMatchingSearchQuery.value.length ? usersMatchingSearchQuery.value : users
</script>

Below are links to the code that created the component with composition API. After running the app, visit the route /banks:

Vue Composition API App

It's your task to add the last logical concern that will manage to filter users.

Happy coding!

Updated 12:05 May 16 '22

If you like this content, please consider buying me coffee.
Thank you for your support!

Become a Patron!