home / skills / vuejs-ai / skills / create-adaptable-composable

create-adaptable-composable skill

/skills/create-adaptable-composable

This skill helps you build adaptable Vue composables that accept plain values, refs, or getters and normalize inputs for predictable reactivity.

npx playbooks add skill vuejs-ai/skills --skill create-adaptable-composable

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
2.6 KB
---
name: create-adaptable-composable
description: Create a library-grade Vue composable that accepts maybe-reactive inputs (MaybeRef / MaybeRefOrGetter) so callers can pass a plain value, ref, or getter. Normalize inputs with toValue()/toRef() inside reactive effects (watch/watchEffect) to keep behavior predictable and reactive. Use this skill when user asks for creating adaptable or reusable composables.
license: MIT
metadata:
  author: github.com/vuejs-ai
  version: "17.0.0"
compatibility: Requires Vue 3 (or above) or Nuxt 3 (or above) project
---

# Create Adaptable Composable

Adaptable composables are reusable functions that can accept both reactive and non-reactive inputs. This allows developers to use the composable in a variety of contexts without worrying about the reactivity of the inputs.

Steps to design an adaptable composable in Vue.js:
1. Confirm the composable's purpose and API design and expected inputs/outputs.
2. Identify inputs params that should be reactive (MaybeRef / MaybeRefOrGetter).
3. Use `toValue()` or `toRef()` to normalize inputs inside reactive effects.
4. Implement the core logic of the composable using Vue's reactivity APIs.

## Core Type Concepts

### Type Utilities

```ts
/**
 * value or writable ref (value/ref/shallowRef/writable computed)
 */
export type MaybeRef<T = any> = T | Ref<T> | ShallowRef<T> | WritableComputedRef<T>;

/**
 * MaybeRef<T> + ComputedRef<T> + () => T
 */
export type MaybeRefOrGetter<T = any> = MaybeRef<T> | ComputedRef<T> | (() => T);
```

### Policy and Rules

- Read-only, computed-friendly input: use `MaybeRefOrGetter`
- Needs to be writable / two-way input: use `MaybeRef`
- Parameter might be a function value (callback/predicate/comparator): do not use `MaybeRefOrGetter`, or you may accidentally invoke it as a getter.
- DOM/Element targets: if you want computed/derived targets, use `MaybeRefOrGetter`.

When `MaybeRefOrGetter` or `MaybeRef` is used: 
- resolve reactive value using `toRef()` (e.g. watcher source)
- resolve non-reactive value using `toValue()`

### Examples

Adaptable `useDocumentTitle` Composable: read-only title parameter

```ts
import { watch, toRef } from 'vue'
import type { MaybeRefOrGetter } from 'vue'

export function useDocumentTitle(title: MaybeRefOrGetter<string>) {
  watch(toRef(title), (t) => {
    document.title = t
  }, { immediate: true })
}
```

Adaptable `useCounter` Composable: two-way writable count parameter

```ts
import { watch, toRef } from 'vue'
import type { MaybeRef } from 'vue'

function useCounter(count: MaybeRef<number>) {
  const countRef = toRef(count)
  function add() {
    countRef.value++
  }
  return { add }
}
```

Overview

This skill shows how to create a library-grade Vue composable that accepts maybe-reactive inputs (MaybeRef / MaybeRefOrGetter). It explains when to accept plain values, refs, or getters and how to normalize them so the composable remains predictable and reactive. The guidance is practical and focused on reusable API design for Vue 3 composables.

How this skill works

Mark parameters that should accept reactive or non-reactive inputs using MaybeRef or MaybeRefOrGetter types. Inside reactive effects (watch / watchEffect) normalize inputs with toRef() when you need a reactive source or toValue() when you need the current value. Implement the composable logic using Vue reactivity APIs so both refs and plain values behave consistently.

When to use it

  • When building composables that must accept plain values, refs, or computed getters without breaking callers.
  • When you want a stable reactive watcher source regardless of whether the caller passed a value or a ref.
  • When a parameter sometimes needs to be writable (two-way) and sometimes read-only.
  • When exposing a library API that should be flexible and ergonomic for different consumer patterns.
  • When a DOM/element target could be derived or computed and still needs to be observed reactively.

Best practices

  • Use MaybeRefOrGetter for read-only inputs that may be computed or static.
  • Use MaybeRef for inputs that must support two-way updates or writable refs.
  • Never mark callback or predicate function parameters as MaybeRefOrGetter to avoid accidental invocation.
  • Inside watchers use toRef(...) for the watcher source and toValue(...) when you only need the immediate value.
  • Provide clear TypeScript types and minimal runtime normalization so behavior is predictable.

Example use cases

  • useDocumentTitle(title: MaybeRefOrGetter<string>) — normalize with toRef inside watch to update document.title.
  • useCounter(count: MaybeRef<number>) — convert toRef for writable increments and two-way updates.
  • useElementVisibility(target: MaybeRefOrGetter<HTMLElement | null>) — watch a normalized ref for visibility changes.
  • useDebouncedValue(value: MaybeRefOrGetter<T>, delay) — derive a reactive debounced value from any input source.

FAQ

When should I use toRef vs toValue?

Use toRef when you need a reactive source for watch/watchEffect. Use toValue when you only need the current non-reactive value or to avoid wrapping a getter.

Can I accept functions as MaybeRefOrGetter?

Avoid it. If a parameter is a callback, treat it as a plain function type so you don't accidentally invoke it as a getter.