Skip to content

Vue 3 Composable Style Guide

This guide is a comprehensive guide to writing Vue 3 composable. It covers the best practices, naming conventions, and patterns to follow when writing Vue 3 composable.

Vue 3 composable is a new feature in Vue 3 that allows you to encapsulate and share logic across components. That composable is easy to write, maintain test, and reuse.

File names

  • Always use use prefix for the composable file name.
  • Use camelCase for the file name.
bash
usePagination.ts
useFetch.ts

Composable name

  • use descriptive name for the composable
typescript
export function usePagination() {}

Directory structure

  • place global composables in the composables directory;
  • place component composables in the same directory as the component.
bash
src/
└── composables/
    ├── usePagination.ts
    └── useUserData.ts
└── components/
    └── AcvUser/
        ├── AcvUser.vue
        └── useUserData.ts

Arguments

  • use object arguments for 4 or more parameters.
typescript
usePagination({ total: 100, pageSize: 10, currentPage: 1, pageCount: 5 });
useFetch('https://api.com', 'GET');

Error handling

  • expose error state and error message.
  • always try to throw error in the composable.
typescript
const error = ref(null);

try {
  // Do something
}
catch (err) {
  error.value = err;
  throw err;
}

return { error };

Do not mix UI and business logic

  • decouple ui from business logic
  • composable should focus on managing state and business logic
  • keep UI logic in the component
typescript
// Good
export function useUserData(userId) {
  const user = ref(null);
  const error = ref(null);

  async function fetchUser() {
    try {
      const response = await axios.get(`/api/users/${userId}`);
      user.value = response.data;
    }
    catch (e) {
      error.value = e;
      throw e;
    }
  };

  return { user, error, fetchUser };
}

// In component
function setup() {
  const { user, error, fetchUser } = useUserData(userId);

  watch(error, (newValue) => {
    if (newValue) {
      showToast('An error occurred.'); // UI logic in component
    }
  });

  return { user, fetchUser };
}

Anatomy

  • use well-defined sections in the composable
  • primary state, main logic that composable is responsible for
  • supportive state, secondary state that composable uses, like hold values, etc.
  • methods, functions that composable uses for updating states
typescript
export function useUserData(userId) {
  // Primary State
  const user = ref(null);
  const error = ref(null);

  // Supportive State
  const status = ref('idle');

  // Methods
  async function fetchUser() {
    status.value = 'loading';
    try {
      const response = await axios.get(`/api/users/${userId}`);

      user.value = response.data;
      status.value = 'success';
    }
    catch (e) {
      status.value = 'error';
      error.value = e;
      throw e;
    }
  };

  return { user, error, fetchUser };
}

Structure

Use such file structure for the composable:

  • Initialization of logic
  • Refs, reactive references
  • Computed properties
  • Methods and functions
  • Lifecycle hooks
  • Watch
  • Return statement
typescript
export default function useCounter() {
  // Initializing
  // Initialize variables, make API calls, or any setup logic
  // For example, using a router
  // ...

  // Refs
  const count = ref(0);

  // Computed
  const isEven = computed(() => count.value % 2 === 0);

  // Methods
  function increment() {
    count.value++;
  };

  function decrement() {
    count.value--;
  };

  // Lifecycle
  onMounted(() => {
    console.log('Counter is mounted');
  });

  return {
    count,
    isEven,
    increment,
    decrement,
  };
}

Functional core, imperative shell

  • keep the core of the composable functional and avoid side effects.
  • use imperative shell for Vue-specific or side effects operations.
typescript
function calculate(a, b) {
  return a + b;
}

// Imperative Shell
export function useCalculator() {
  const result = ref(0);

  function add(a, b) {
    result.value = calculate(a, b); // Using the functional core
  };

  // Other side-effecting code can go here, e.g., logging, API calls

  return { result, add };
}

Single responsibility

  • composable should have a single responsibility.
  • decompose complex logic into smaller composables.
typescript
export function useCounter() {
  const count = ref(0);

  function increment() {
    count.value++;
  };

  function decrement() {
    count.value--;
  };

  return { count, increment, decrement };
}

MIT Licensed