Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/composable-options-object-pattern.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

5.1 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Use Options Object Pattern for Composable Parameters MEDIUM Long parameter lists are error-prone and unclear; options objects are self-documenting and extensible best-practice
vue3
composables
composition-api
api-design
typescript
patterns

Use Options Object Pattern for Composable Parameters

Impact: MEDIUM - When a composable accepts multiple parameters (especially optional ones), use an options object instead of positional arguments. This makes the API self-documenting, prevents argument order mistakes, and allows easy extension without breaking changes.

Task Checklist

  • Use options object when composable has more than 2-3 parameters
  • Always use options object when most parameters are optional
  • Provide sensible defaults via destructuring
  • Type the options object for better IDE support
  • Required parameters can be positional; optional ones in options

Incorrect:

// WRONG: Many positional parameters - unclear and error-prone
export function useFetch(url, method, headers, timeout, retries, onError) {
  // What was the 4th parameter again?
}

// Usage - which boolean is which?
const { data } = useFetch('/api/users', 'GET', null, 5000, 3, handleError);

// WRONG: Easy to get order wrong
export function useDebounce(value, delay, immediate, maxWait) {
  // ...
}

// Is 500 the delay or maxWait? Is true immediate?
const debounced = useDebounce(searchQuery, 500, true, 1000);

Correct:

// CORRECT: Options object pattern
export function useFetch(url, options = {}) {
  const {
    method = 'GET',
    headers = {},
    timeout = 30000,
    retries = 0,
    onError = null,
    immediate = true
  } = options

  // Implementation...
}

// Usage - clear and self-documenting
const { data } = useFetch('/api/users', {
  method: 'POST',
  timeout: 5000,
  retries: 3,
  onError: handleError
})

// CORRECT: With TypeScript for better IDE support
interface UseFetchOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  headers?: Record<string, string>
  timeout?: number
  retries?: number
  onError?: (error: Error) => void
  immediate?: boolean
}

export function useFetch(url: MaybeRefOrGetter<string>, options: UseFetchOptions = {}) {
  const {
    method = 'GET',
    headers = {},
    timeout = 30000,
    retries = 0,
    onError = null,
    immediate = true
  } = options

  // TypeScript now provides autocomplete for options
}

Pattern: Required + Options

Keep truly required parameters positional, bundle optional ones:

// url is always required, options are not
export function useFetch(url, options = {}) {
  // ...
}

// Both key and storage are required for this to make sense
export function useStorage(key, storage, options = {}) {
  const { serializer = JSON, deep = true } = options;
  // ...
}

// Usage
useStorage('user-prefs', localStorage, { deep: false });

Pattern: Reactive Options

Options can also be reactive for dynamic behavior:

export function useFetch(url, options = {}) {
  const {
    refetch = ref(true), // Can be a ref!
    interval = null,
  } = options;

  watchEffect(() => {
    if (toValue(refetch)) {
      // Perform fetch
    }
  });
}

// Usage with reactive option
const shouldFetch = ref(true);
const { data } = useFetch('/api/data', { refetch: shouldFetch });

// Later, disable fetching
shouldFetch.value = false;

Pattern: Returning Configuration

Options objects also work well for return values:

export function useCounter(options = {}) {
  const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options;

  const count = ref(initial);

  function increment() {
    count.value = Math.min(count.value + step, max);
  }

  function decrement() {
    count.value = Math.max(count.value - step, min);
  }

  function set(value) {
    count.value = Math.min(Math.max(value, min), max);
  }

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

// Clear, readable usage
const { count, increment, decrement } = useCounter({
  initial: 10,
  min: 0,
  max: 100,
  step: 5,
});

VueUse Convention

VueUse uses this pattern extensively:

import { useDebounceFn, useThrottleFn, useLocalStorage } from '@vueuse/core';

// All use options objects
const debouncedFn = useDebounceFn(fn, 1000, { maxWait: 5000 });

const throttledFn = useThrottleFn(fn, 1000, { trailing: true, leading: false });

const state = useLocalStorage('key', defaultValue, {
  deep: true,
  listenToStorageChanges: true,
  serializer: {
    read: JSON.parse,
    write: JSON.stringify,
  },
});

Anti-pattern: Boolean Trap

Options objects prevent the "boolean trap":

// BAD: What do these booleans mean?
useModal(true, false, true);

// GOOD: Self-documenting
useModal({
  closable: true,
  backdrop: false,
  keyboard: true,
});

Reference