Vue 3 Composition API: Best Practices for Modern Development

mubaidr
December 12, 2024
Vue.jsJavaScriptFrontendComposition API
Vue 3 Composition API: Best Practices for Modern Development

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.