Vue Composition API: A Game Changer for Large Apps
Vue’s Composition API RFC dropped last week. It’s the biggest change to Vue since 2.0.
The Problem
Options API works great for small components:
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('mounted')
}
}
But for large components, related logic is scattered:
export default {
data() {
return {
// User feature
user: null,
userLoading: false,
// Posts feature
posts: [],
postsLoading: false,
// Comments feature
comments: [],
commentsLoading: false
}
},
methods: {
// User feature
async fetchUser() { },
// Posts feature
async fetchPosts() { },
// Comments feature
async fetchComments() { }
},
mounted() {
// User feature
this.fetchUser()
// Posts feature
this.fetchPosts()
// Comments feature
this.fetchComments()
}
}
Logic for one feature is split across data, methods, and lifecycle hooks.
The Solution: Composition API
Group related logic together:
import { ref, onMounted } from 'vue'
export default {
setup() {
// User feature
const user = ref(null)
const userLoading = ref(false)
async function fetchUser() {
userLoading.value = true
user.value = await api.getUser()
userLoading.value = false
}
onMounted(fetchUser)
// Posts feature
const posts = ref([])
const postsLoading = ref(false)
async function fetchPosts() {
postsLoading.value = true
posts.value = await api.getPosts()
postsLoading.value = false
}
onMounted(fetchPosts)
return {
user,
userLoading,
posts,
postsLoading
}
}
}
All user-related logic is together. All posts-related logic is together.
Composition Functions
Extract logic into reusable functions:
// useUser.js
import { ref, onMounted } from 'vue'
export function useUser() {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetch() {
loading.value = true
try {
user.value = await api.getUser()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
onMounted(fetch)
return {
user,
loading,
error,
refetch: fetch
}
}
// Component.vue
import { useUser } from './useUser'
export default {
setup() {
const { user, loading, error, refetch } = useUser()
return {
user,
loading,
error,
refetch
}
}
}
Real-World Example
We refactored our user profile component:
Before (Options API): 300 lines, logic scattered
After (Composition API): 150 lines, logic organized
// useUserProfile.js
export function useUserProfile(userId) {
const user = ref(null)
const loading = ref(false)
async function fetch() {
loading.value = true
user.value = await api.getUser(userId)
loading.value = false
}
async function update(data) {
await api.updateUser(userId, data)
await fetch()
}
onMounted(fetch)
return { user, loading, update }
}
// usePosts.js
export function usePosts(userId) {
const posts = ref([])
const loading = ref(false)
async function fetch() {
loading.value = true
posts.value = await api.getPosts(userId)
loading.value = false
}
async function create(post) {
await api.createPost(userId, post)
await fetch()
}
onMounted(fetch)
return { posts, loading, create }
}
// UserProfile.vue
export default {
setup() {
const userId = route.params.id
const userProfile = useUserProfile(userId)
const userPosts = usePosts(userId)
return {
...userProfile,
...userPosts
}
}
}
Each composition function is:
- Focused on one feature
- Testable in isolation
- Reusable across components
Reactivity
ref
For primitive values:
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
reactive
For objects:
const state = reactive({
count: 0,
name: 'John'
})
console.log(state.count) // 0 (no .value needed)
state.count++
computed
const count = ref(0)
const doubled = computed(() => count.value * 2)
console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10
watch
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
count.value++ // Logs: "Count changed from 0 to 1"
Lifecycle Hooks
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('mounted')
})
onUpdated(() => {
console.log('updated')
})
onUnmounted(() => {
console.log('unmounted')
})
}
}
TypeScript Support
Composition API has better TypeScript support:
import { ref, Ref } from 'vue'
interface User {
id: number
name: string
}
export function useUser(): {
user: Ref<User | null>
loading: Ref<boolean>
fetch: () => Promise<void>
} {
const user = ref<User | null>(null)
const loading = ref(false)
async function fetch() {
loading.value = true
user.value = await api.getUser()
loading.value = false
}
return { user, loading, fetch }
}
Full type inference!
Comparison with React Hooks
Similar concept, different implementation:
React Hooks:
- Must follow rules (no conditionals, no loops)
- Re-run on every render
- Dependency arrays
Vue Composition API:
- No special rules
- Run once in setup()
- Automatic dependency tracking
Should You Use It?
Yes, if:
- Building large components
- Want better code organization
- Need TypeScript support
- Want to share logic between components
Stick with Options API if:
- Building small components
- Team prefers Options API
- Don’t have complex logic
Both APIs will be supported. You can mix them.
When Will It Be Available?
- Vue 2.x: Plugin available now (@vue/composition-api)
- Vue 3.0: Built-in (coming Q1 2020)
Our Plan
- Try it in new components
- Refactor complex components gradually
- Create composition function library
- Wait for Vue 3.0 for full adoption
The Verdict
Composition API solves real problems in large Vue apps. Better code organization, better reusability, better TypeScript support.
It’s not replacing Options API - it’s an addition. Use what works for your use case.
I’m excited about this. It’s going to make Vue even better.
Questions? Let me know!