home / skills / serkodev / vue-skills / create-agnostic-composable

create-agnostic-composable skill

/skills/create-agnostic-composable

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

npx playbooks add skill serkodev/vue-skills --skill create-agnostic-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-agnostic-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 agnostic or reusable composables.
license: MIT
metadata:
    author: SerKo <https://github.com/serkodev>
    version: "0.1"
compatibility: Requires Vue 3 (or above) or Nuxt 3 (or above) project
---

# Create Agnostic Composable

Agnostic 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 agnostic 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

Agnostic `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 })
}
```

Agnostic `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 teaches how to create library-grade Vue composables that accept maybe-reactive inputs (MaybeRef / MaybeRefOrGetter). It explains when to use toRef() and toValue() inside reactive effects so callers can pass plain values, refs, or getter functions. The goal is predictable, reusable composables that behave correctly in both reactive and non-reactive contexts.

How this skill works

Design the composable API first and mark parameters that should accept reactive or non-reactive inputs. Inside reactive effects (watch, watchEffect), normalize inputs with toRef() for watcher sources and toValue() when you need a snapshot. Choose MaybeRef for writable two-way inputs and MaybeRefOrGetter for read-only or computed-friendly inputs. Implement core logic using Vue reactivity primitives so the composable remains agnostic to caller input type.

When to use it

  • Building composables for public libraries or shared components
  • Accepting both refs and plain values from callers
  • Creating composables that may receive computed/getter sources
  • Designing two-way bindings where callers may pass writable refs
  • When you need predictable, testable reactive behavior across call sites

Best practices

  • Decide whether parameter must be writable (use MaybeRef) or read-only/computed-friendly (use MaybeRefOrGetter)
  • Inside watch/watchEffect resolve inputs via toRef() for reactive sources and toValue() for non-reactive snapshots
  • Avoid treating function parameters as getters unless they are explicitly intended to be invoked
  • Document parameter expectations (writable vs read-only) in the composable API
  • Keep core logic isolated so normalization happens at the edges of reactive effects

Example use cases

  • useDocumentTitle(title: MaybeRefOrGetter<string>) that updates document.title reactively for ref, computed, or plain string
  • useCounter(count: MaybeRef<number>) that increments a caller-provided writable ref or ref-like value
  • useElementSize(target: MaybeRefOrGetter<Element | null>) that watches a ref, computed element, or plain element reference
  • useRemoteData(options: MaybeRefOrGetter<RequestOptions>) where callers can pass static or computed request options
  • A form field composable that accepts a writable MaybeRef for two-way binding and normalizes it internally

FAQ

When should I use toRef() vs toValue()?

Use toRef() when you need a reactive Ref source inside watch/watchEffect (so updates propagate). Use toValue() when you only need a current snapshot and do not need reactivity from a non-ref input.

What if a parameter can be a function callback?

Do not mark callback parameters as MaybeRefOrGetter unless the function is intentionally a getter. Treat callbacks as plain functions to avoid accidental invocation during normalization.