Back to the blog

Practical Vue Composition API Patterns

Reusable patterns for composables, shared state, and cleaner component logic in Vue 3.


Practical Vue Composition API Patterns

The Composition API shines when logic grows beyond a single component. Here are patterns I reach for often on Vue and Nuxt projects.

Extract early, keep it focused

If a piece of state and its handlers appear in more than one place—or the <script setup> block gets hard to scan—move it into a composable:

// composables/useToggle.js
export function useToggle(initial = false) {
  const on = ref(initial);
  const toggle = () => { on.value = !on.value; };
  const set = (value) => { on.value = Boolean(value); };
  return { on, toggle, set };
}

Keep composables small. One concern per file beats a mega-useApp that does everything.

Prefer explicit returns

Return only what the consumer needs. That keeps the API clear and makes refactors safer:

export function useFetchPosts() {
  const posts = ref([]);
  const pending = ref(false);
  const error = ref(null);

  async function load() {
    pending.value = true;
    error.value = null;
    try {
      posts.value = await $fetch('/blog-posts.json');
    } catch (e) {
      error.value = e;
    } finally {
      pending.value = false;
    }
  }

  return { posts, pending, error, load };
}

Shared state without a global store

For light cross-component state, a module-level ref inside a composable is enough:

const theme = ref('system');

export function useTheme() {
  function setTheme(next) {
    theme.value = next;
  }
  return { theme, setTheme };
}

Reach for Pinia when you need persistence, DevTools, or many consumers with complex mutations.

Wrap-up

Composition API patterns are less about cleverness and more about boundaries: extract early, return explicitly, and share state only when it earns its keep.