How to Use Composition Api in Vue

How to Use Composition API in Vue The Vue.js framework has evolved significantly since its initial release, introducing powerful new patterns to help developers build scalable, maintainable, and performant applications. One of the most transformative additions in Vue 3 is the Composition API . Unlike the Options API — which organizes component logic by predefined options like data , methods , and

Nov 10, 2025 - 08:44
Nov 10, 2025 - 08:44
 1

How to Use Composition API in Vue

The Vue.js framework has evolved significantly since its initial release, introducing powerful new patterns to help developers build scalable, maintainable, and performant applications. One of the most transformative additions in Vue 3 is the Composition API. Unlike the Options API which organizes component logic by predefined options like data, methods, and computed the Composition API allows you to group related logic by feature or concern, making code more readable, reusable, and easier to test.

As applications grow in complexity, the Options API can become unwieldy. Logic for a single feature such as form validation, user authentication, or real-time data syncing may be scattered across multiple options, making it difficult to track and maintain. The Composition API solves this by letting you write logic in a more natural, function-based way, where everything related to a specific feature lives together in one place.

This tutorial provides a comprehensive, step-by-step guide to mastering the Composition API in Vue 3. Whether you're new to Vue or transitioning from Vue 2, this guide will equip you with the knowledge and practical skills to leverage the Composition API effectively. Well explore its core concepts, demonstrate real-world usage, outline best practices, recommend essential tools, and answer common questions to ensure you can confidently adopt this modern approach in your projects.

Step-by-Step Guide

Understanding the Setup Function

The heart of the Composition API is the setup() function. This function is called before the component is created, and it runs in the context of the components instance but before any of the Options API options are initialized. Its the entry point for writing Composition API logic.

To use the Composition API, you define a component using the setup() function. This function receives two arguments: props and context. The props object contains all the props passed to the component, and context is an object that exposes emit, slots, and attrs.

Heres a basic example:

<template>

<div>

<h2>{{ title }}</h2>

<p>Count: {{ count }}</p>

<button @click="increment">Increment</button>

</div>

</template>

<script>

import { ref } from 'vue'

export default {

setup(props, context) {

const count = ref(0)

const title = 'Composition API Demo'

const increment = () => {

count.value++

}

return {

count,

title,

increment

}

}

}

</script>

In this example, ref is used to create a reactive state variable count. The increment function modifies this state, and both are returned from setup() so they can be accessed in the template. Notice that count.value is used to read or update the value this is because ref returns an object with a value property.

Using Ref and Reactive

Reactivity is the cornerstone of Vue. The Composition API provides two primary tools for creating reactive state: ref() and reactive().

Ref is used for primitive values (strings, numbers, booleans) and objects. It wraps the value in a reactive object with a value property:

import { ref } from 'vue'

const message = ref('Hello Vue!')

const user = ref({ name: 'Alice', age: 30 })

console.log(message.value) // 'Hello Vue!'

user.value.name = 'Bob' // update the object

Reactive is used for objects and arrays. It creates a reactive proxy of the original object, allowing you to access properties directly without using .value:

import { reactive } from 'vue'

const state = reactive({

count: 0,

user: { name: 'Alice' }

})

console.log(state.count) // 0

state.count++ // no .value needed

Choose ref for simple values or when you need to pass reactive values across components. Use reactive for complex objects where you want to avoid the .value syntax.

Computed Properties with Computed

Computed properties are derived values that automatically update when their dependencies change. In the Composition API, you use the computed() function to define them.

import { ref, computed } from 'vue'

const count = ref(0)

const doubleCount = computed(() => count.value * 2)

console.log(doubleCount.value) // 0

count.value++

console.log(doubleCount.value) // 2

Like ref, computed() returns a reactive object with a value property. However, unlike methods, computed properties are cached based on their dependencies they only re-evaluate when a dependency changes.

You can also create writable computed properties by returning an object with get and set functions:

const fullName = computed({

get: () => ${firstName.value} ${lastName.value},

set: (newValue) => {

const names = newValue.split(' ')

firstName.value = names[0]

lastName.value = names[1]

}

})

Watchers and WatchEffect

Vues reactivity system also allows you to respond to changes in state using watchers. The Composition API offers two types: watch() and watchEffect().

WatchEffect automatically tracks dependencies and runs the function whenever any reactive property it uses changes:

import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {

console.log(Count is now: ${count.value})

})

count.value++ // logs: "Count is now: 1"

This is ideal for side effects like logging, making API calls, or updating the DOM.

Watch gives you more control. You specify the exact source to watch and can access both the old and new values:

import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {

console.log(Count changed from ${oldVal} to ${newVal})

})

count.value = 5 // logs: "Count changed from 0 to 5"

You can also watch multiple sources:

watch([count, name], ([newCount, newName], [oldCount, oldName]) => {

console.log(Count: ${oldCount} ? ${newCount}, Name: ${oldName} ? ${newName})

})

Managing Lifecycle Hooks

The Composition API provides equivalent functions for Vues lifecycle hooks. These functions are called directly inside setup() and execute when the component reaches that lifecycle stage.

Heres how to map traditional Options API hooks to Composition API equivalents:

  • beforeCreate ? Not needed (setup runs before)
  • created ? Not needed (setup runs after)
  • beforeMount ? onBeforeMount()
  • mounted ? onMounted()
  • beforeUpdate ? onBeforeUpdate()
  • updated ? onUpdated()
  • beforeUnmount ? onBeforeUnmount()
  • unmounted ? onUnmounted()

Example using onMounted and onUnmounted:

import { onMounted, onUnmounted } from 'vue'

export default {

setup() {

onMounted(() => {

console.log('Component mounted!')

// Add event listeners, start timers, etc.

})

onUnmounted(() => {

console.log('Component unmounted!')

// Clean up event listeners, timers, subscriptions

})

}

}

Always clean up side effects in onUnmounted to prevent memory leaks especially when using timers, intervals, or external subscriptions.

Using Provide and Inject

For passing data down the component tree without prop drilling, Vue provides provide() and inject().

In a parent component:

import { provide, ref } from 'vue'

export default {

setup() {

const theme = ref('dark')

const toggleTheme = () => {

theme.value = theme.value === 'dark' ? 'light' : 'dark'

}

provide('theme', theme)

provide('toggleTheme', toggleTheme)

return {}

}

}

In any child component, deep down the tree:

import { inject } from 'vue'

export default {

setup() {

const theme = inject('theme')

const toggleTheme = inject('toggleTheme')

return { theme, toggleTheme }

}

}

Use provide/inject sparingly its best for global or deeply nested state like themes, user preferences, or shared services.

Working with Templates and Slots

When using the Composition API, you can still use templates normally. However, if you need to access slots or attributes, you must use the context parameter in setup():

export default {

setup(props, { slots, attrs }) {

// Access named slots

const headerSlot = slots.header

const defaultSlot = slots.default

// Access non-prop attributes

console.log(attrs.class) // e.g., 'btn-primary'

return {}

}

}

For dynamic slot content, you can return slot functions from setup() and render them in the template:

<template>

<div>

<header>{{ headerSlot() }}</header>

<main>{{ defaultSlot() }}</main>

</div>

</template>

Best Practices

Group Logic by Feature, Not by Type

One of the main advantages of the Composition API is the ability to organize code by feature rather than by option type. Instead of scattering related logic across data, methods, and computed, group them together.

For example, instead of:

export default {

data() {

return {

user: null,

loading: false,

error: null

}

},

methods: {

async fetchUser() {

this.loading = true

try {

this.user = await api.getUser()

} catch (err) {

this.error = err.message

} finally {

this.loading = false

}

}

},

computed: {

isLoggedIn() {

return !!this.user

}

}

}

Use the Composition API to group all user-related logic:

import { ref, computed } from 'vue'

export default {

setup() {

const user = ref(null)

const loading = ref(false)

const error = ref(null)

const fetchUser = async () => {

loading.value = true

error.value = null

try {

user.value = await api.getUser()

} catch (err) {

error.value = err.message

} finally {

loading.value = false

}

}

const isLoggedIn = computed(() => !!user.value)

return {

user,

loading,

error,

fetchUser,

isLoggedIn

}

}

}

This structure makes it immediately clear what logic belongs to the user feature improving readability and maintainability.

Extract Logic into Composables

Composables are reusable functions that encapsulate logic using the Composition API. They are the key to code reuse and modularization.

Create a useUser.js file:

// composables/useUser.js

import { ref, computed } from 'vue'

import api from '@/api'

export function useUser() {

const user = ref(null)

const loading = ref(false)

const error = ref(null)

const fetchUser = async () => {

loading.value = true

error.value = null

try {

user.value = await api.getUser()

} catch (err) {

error.value = err.message

} finally {

loading.value = false

}

}

const isLoggedIn = computed(() => !!user.value)

return {

user,

loading,

error,

fetchUser,

isLoggedIn

}

}

Then use it in any component:

import { defineComponent } from 'vue'

import { useUser } from '@/composables/useUser'

export default defineComponent({

setup() {

const { user, loading, error, fetchUser, isLoggedIn } = useUser()

return {

user,

loading,

error,

fetchUser,

isLoggedIn

}

}

})

Composables follow a naming convention: useX (e.g., useAuth, useLocalStorage, useFetch). They should be pure functions no side effects outside their scope and should not assume theyre used inside a component.

Use TypeScript for Type Safety

The Composition API works exceptionally well with TypeScript. Define types for props, state, and return values to catch errors early and improve developer experience.

import { ref, computed } from 'vue'

interface User {

id: number

name: string

email: string

}

export function useUser() {

const user = ref<User | null>(null)

const loading = ref(false)

const fetchUser = async (id: number) => {

loading.value = true

try {

const response = await api.getUser(id)

user.value = response.data

} finally {

loading.value = false

}

}

const isLoggedIn = computed(() => user.value !== null)

return {

user,

loading,

fetchUser,

isLoggedIn

}

}

Vue 3s official TypeScript support is excellent. Use defineProps and defineEmits for type-safe props and events:

import { defineComponent, defineProps, defineEmits } from 'vue'

const props = defineProps({

title: String,

count: Number

})

const emit = defineEmits(['update:count'])

const increment = () => {

emit('update:count', props.count + 1)

}

Minimize Template Logic

Keep your templates clean and declarative. Avoid complex expressions or logic inside templates. Move logic into computed properties or methods.

Bad:

<template>

<div>

<p>{{ user ? (user.name.toUpperCase() + ' (' + user.age + ')') : 'No user' }}</p>

</div>

</template>

Good:

<template>

<div>

<p>{{ userDisplay }}</p>

</div>

</template>

<script>

import { computed } from 'vue'

export default {

setup() {

const user = ref({ name: 'Alice', age: 30 })

const userDisplay = computed(() => {

return user.value

? ${user.value.name.toUpperCase()} (${user.value.age})

: 'No user'

})

return { user, userDisplay }

}

}

</script>

Handle Async Operations Carefully

When fetching data, always handle loading and error states. Use computed properties to derive UI states from your reactive data.

const loading = ref(false)

const error = ref(null)

const data = ref(null)

const fetchData = async () => {

loading.value = true

error.value = null

try {

data.value = await api.getData()

} catch (err) {

error.value = err.message

} finally {

loading.value = false

}

}

const hasData = computed(() => data.value !== null)

const isLoading = computed(() => loading.value)

const hasError = computed(() => error.value !== null)

Then in the template:

<template>

<div v-if="isLoading">Loading...</div>

<div v-else-if="hasError">Error: {{ error }}</div>

<div v-else-if="hasData">{{ data }}</div>

<div v-else>No data available.</div>

</template>

Tools and Resources

Vue DevTools

The Vue DevTools browser extension is indispensable when working with the Composition API. It allows you to inspect reactive state, computed properties, and even composables. You can see the values of ref and reactive objects in real time, track reactivity dependencies, and debug watchers and effects.

Install it from your browsers extension store (Chrome, Firefox, Edge) and ensure youre using Vue 3. DevTools will automatically detect Composition API usage and display it under the Components and Reactivity tabs.

Vue CLI and Vite

While Vue CLI is still supported, Vite is now the recommended build tool for Vue 3 projects. Vite offers blazing-fast cold starts, instant hot module replacement (HMR), and excellent TypeScript support out of the box.

To create a new Vue 3 + Vite project:

npm create vue@latest my-project

Follow the prompts to enable TypeScript, ESLint, and other features. Vites configuration is minimal and intuitive, making it ideal for modern Vue development.

ESLint and Prettier

Use ESLint with the eslint-plugin-vue and @vue/eslint-config-typescript plugins to enforce consistent code style and catch potential bugs. Combine it with Prettier for automatic code formatting.

Install dependencies:

npm install -D eslint eslint-plugin-vue @vue/eslint-config-typescript prettier eslint-plugin-prettier eslint-config-prettier

Configure .eslintrc.cjs:

module.exports = {

extends: [

'eslint:recommended',

'plugin:vue/vue3-recommended',

'@vue/eslint-config-typescript'

],

rules: {

'prettier/prettier': 'error'

}

}

And .prettierrc:

{

"semi": true,

"singleQuote": true,

"printWidth": 80

}

Vue Use Library

The VueUse library is a collection of over 150 ready-to-use composables for common tasks like useLocalStorage, useMouse, useFetch, useDebounce, and more. Its built on top of the Composition API and is fully typed for TypeScript.

Install:

npm install @vueuse/core

Use:

import { useLocalStorage } from '@vueuse/core'

const theme = useLocalStorage('theme', 'dark')

VueUse eliminates boilerplate and follows best practices its an excellent resource for accelerating development.

Vue Mastery and Vue School

For structured learning, consider these platforms:

  • Vue Mastery Offers in-depth courses on Vue 3 and the Composition API, including real-world projects.
  • Vue School Short, practical video tutorials with downloadable code.

Both platforms have free and paid content and are highly recommended for developers serious about mastering Vue 3.

Real Examples

Example 1: Form Validation with Composition API

Building a form with validation is a common use case. Heres how to create a reusable, testable validation system using composables.

// composables/useFormValidation.js

import { ref, computed } from 'vue'

export function useFormValidation(initialValues, rules) {

const form = ref(initialValues)

const errors = ref({})

const validate = () => {

errors.value = {}

Object.keys(rules).forEach(field => {

const value = form.value[field]

const rule = rules[field]

if (rule && !rule(value)) {

errors.value[field] = rule.message

}

})

return Object.keys(errors.value).length === 0

}

const isValid = computed(() => Object.keys(errors.value).length === 0)

const reset = () => {

form.value = { ...initialValues }

errors.value = {}

}

return {

form,

errors,

validate,

isValid,

reset

}

}

Use it in a login component:

// components/LoginForm.vue

import { defineComponent } from 'vue'

import { useFormValidation } from '@/composables/useFormValidation'

export default defineComponent({

setup() {

const initialValues = { email: '', password: '' }

const rules = {

email: {

validate: (value) => value.includes('@'),

message: 'Email must contain @'

},

password: {

validate: (value) => value.length >= 6,

message: 'Password must be at least 6 characters'

}

}

const { form, errors, validate, isValid, reset } = useFormValidation(initialValues, rules)

const handleSubmit = async () => {

if (validate()) {

// Submit form

console.log('Form submitted:', form.value)

reset()

}

}

return {

form,

errors,

handleSubmit,

isValid

}

}

})

Template:

<template>

<form @submit.prevent="handleSubmit">

<div>

<label>Email</label>

<input v-model="form.email" type="email" />

<span v-if="errors.email" class="error">{{ errors.email }}</span>

</div>

<div>

<label>Password</label>

<input v-model="form.password" type="password" />

<span v-if="errors.password" class="error">{{ errors.password }}</span>

</div>

<button :disabled="!isValid">Login</button>

</form>

</template>

Example 2: Real-Time Data with WebSockets

Lets create a composable for real-time stock price updates using WebSocket:

// composables/useStockPrice.js

import { ref, onMounted, onUnmounted } from 'vue'

export function useStockPrice(symbol) {

const price = ref(0)

const isConnected = ref(false)

let socket = null

const connect = () => {

socket = new WebSocket(wss://api.stockstream.com/${symbol})

socket.onopen = () => {

isConnected.value = true

}

socket.onmessage = (event) => {

price.value = parseFloat(event.data)

}

socket.onclose = () => {

isConnected.value = false

}

}

const disconnect = () => {

if (socket) {

socket.close()

}

}

onMounted(() => {

connect()

})

onUnmounted(() => {

disconnect()

})

return {

price,

isConnected

}

}

Use it in a component:

<template>

<div>

<h3>Stock: {{ symbol }}</h3>

<p>Current Price: ${{ price }}</p>

<p v-if="!isConnected">Connecting...</p>

</div>

</template>

<script>

import { defineComponent } from 'vue'

import { useStockPrice } from '@/composables/useStockPrice'

export default defineComponent({

setup() {

const symbol = 'AAPL'

const { price, isConnected } = useStockPrice(symbol)

return { symbol, price, isConnected }

}

})

</script>

FAQs

Is the Composition API better than the Options API?

The Composition API isnt inherently better its different. It excels in large, complex applications where logic reuse and organization are critical. For small components or simple UIs, the Options API remains clean and straightforward. Vue 3 supports both, so you can mix them in the same project. Choose based on your teams needs and project scale.

Can I use the Composition API in Vue 2?

Yes, via the Vue Composition API plugin. It backports most Composition API features to Vue 2.6+. However, its recommended to upgrade to Vue 3 for full native support, better performance, and ongoing updates.

Do I need to use TypeScript with the Composition API?

No, TypeScript is optional. But it significantly improves developer experience, reduces bugs, and enhances code maintainability especially in large teams. If youre building a production application, using TypeScript with the Composition API is strongly recommended.

Can I use the Composition API with Vue Router and Pinia?

Absolutely. Vue Router 4 and Pinia (the official state management library for Vue 3) are both designed to work seamlessly with the Composition API. Pinia, in particular, uses composables internally and encourages a function-based approach to managing state.

How do I test components using the Composition API?

Testing is straightforward. You can test composables in isolation since theyre just functions and test components using libraries like Vue Test Utils. For example:

import { useUser } from '@/composables/useUser'

import { ref } from 'vue'

describe('useUser', () => {

it('returns user data after fetch', async () => {

const { user, fetchUser } = useUser()

const mockUser = { id: 1, name: 'John' }

api.getUser = jest.fn().resolvedValue(mockUser)

await fetchUser()

expect(user.value).toEqual(mockUser)

})

})

Whats the performance impact of using the Composition API?

The Composition API has a slight overhead due to reactive proxies and function calls, but in practice, performance differences are negligible. Vue 3s reactivity system is more efficient overall than Vue 2s, and the Composition API enables better tree-shaking and code splitting, leading to smaller bundle sizes.

Conclusion

The Composition API represents a major evolution in Vue.js development. By organizing logic around features rather than options, it empowers developers to write more maintainable, reusable, and scalable code. Through the use of ref, reactive, computed, watch, and composables, you gain fine-grained control over reactivity and side effects all while keeping your components clean and focused.

Adopting the Composition API isnt just about learning new syntax its about shifting your mindset to write code thats modular, testable, and future-proof. The tools and resources available from VueUse to Vite and TypeScript make it easier than ever to build modern Vue applications with confidence.

Start small: refactor one component using the Composition API. Then extract its logic into a composable. Gradually, youll find yourself naturally gravitating toward this approach and youll wonder how you ever built Vue apps without it.

As Vue 3 continues to mature and gain adoption, the Composition API will become the standard for professional Vue development. Mastering it now ensures youre prepared for the future of web applications built with Vue.