Vue 3 Composition API: Best Practices for Modern Development
The Vue 3 Composition API has revolutionized how we write Vue applications, offering better logic reuse, improved TypeScript support, and more flexible component organization. After working with it in production for over two years, here are the essential best practices I've discovered.
Why Composition API Matters
The Composition API addresses several limitations of the Options API:
- Better Logic Reuse: Composables make it easy to extract and share reactive logic
- Improved TypeScript Support: Better type inference and IDE support
- Flexible Organization: Group related logic together instead of separating by option type
Essential Best Practices
1. Use Composables for Reusable Logic
// composables/useCounter.js
import { ref, computed } from "vue"
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => (count.value = initialValue)
const isEven = computed(() => count.value % 2 === 0)
return {
count: readonly(count),
increment,
decrement,
reset,
isEven,
}
}
2. Organize Code by Feature, Not by Type
Instead of grouping all refs together, group related logic:
// ❌ Avoid - grouped by type
export default {
setup() {
// All refs
const user = ref(null)
const posts = ref([])
const loading = ref(false)
// All computed
const userDisplayName = computed(...)
const sortedPosts = computed(...)
// All methods
const fetchUser = () => {}
const createPost = () => {}
}
}
// ✅ Better - grouped by feature
export default {
setup() {
// User management
const user = ref(null)
const userDisplayName = computed(...)
const fetchUser = () => {}
// Posts management
const posts = ref([])
const sortedPosts = computed(...)
const createPost = () => {}
// UI state
const loading = ref(false)
}
}
3. Use readonly
for Exposed State
When creating composables, expose state as readonly to prevent external mutations:
export function useUserStore() {
const users = ref([])
const loading = ref(false)
const addUser = (user) => {
users.value.push(user)
}
return {
users: readonly(users),
loading: readonly(loading),
addUser,
}
}
Advanced Patterns
Custom Ref Patterns
Create custom refs for complex state management:
import { customRef } from "vue"
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
},
}
})
}
Lifecycle Composition
Combine lifecycle hooks with reactive state:
export function useAsyncData(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
onMounted(fetchData)
return { data, error, loading, refetch: fetchData }
}
Performance Considerations
1. Use shallowRef
for Large Objects
For large objects that don't need deep reactivity:
import { shallowRef } from "vue"
const largeDataSet = shallowRef({
// Large object with many properties
})
2. Optimize with markRaw
For objects that should never be reactive:
import { markRaw } from "vue"
const thirdPartyLibrary = markRaw(new SomeLibrary())
Testing Composables
Composables are easy to test in isolation:
import { describe, it, expect } from "vitest"
import { useCounter } from "@/composables/useCounter"
describe("useCounter", () => {
it("should increment count", () => {
const { count, increment } = useCounter(0)
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
})
Conclusion
The Composition API is a powerful tool that, when used correctly, can significantly improve your Vue.js applications. Focus on creating reusable composables, organizing code by feature, and leveraging TypeScript for better development experience.
The key is to think in terms of logical concerns rather than component options, leading to more maintainable and testable code.