Writing One

Move refs, computed values and the functions that change them into a use-prefixed file, and return refs rather than values.

Syntaxconst { count, double, increment } = useCounter(0)

Logic gets extracted when a second component needs it, or when one component's <script setup> has grown two unrelated jobs. The move is mechanical: create a file under src/composables/, export a function whose name starts with use, cut the refs, computed values and functions into it, and return what the template needs.

What to return

Return an object whose values are refs and functions, never the unwrapped numbers and strings. A ref read in a template is unwrapped for you, so {{ count }} works; in script it is still count.value. Return computed values for anything derived — they are read-only, so a caller cannot corrupt your state by assigning to them. Keep the mutations inside the composable and expose them as functions.

Reactive objects need toRefs

If the state inside is a reactive object, spread toRefs(state) on the way out. Destructuring a reactive object copies the current values and the copies never change. toRefs hands back one ref per property, each still connected to the object.

What trips people up

Each call creates its own state, so two components calling useCounter() get two independent counts. State that must be shared has to be created once, outside the function body, so every caller closes over the same refs — a later stage covers the tool built for that.

Example

Example · vue
<script setup lang="ts">
import { useCounter } from '../composables/useCounter'

const props = defineProps<{ label: string }>()
const { count, double, increment, reset } = useCounter(0, 2)
</script>

<template>
  <section>
    <p>{{ props.label }}</p>
    <p>{{ count }} (double {{ double }})</p>
    <button type="button" @click="increment">Add</button>
    <button type="button" @click="reset">Reset</button>
  </section>
</template>

<style scoped>
section {
  display: grid;
  gap: 0.5rem;
}
</style>

When to use it

  • Pull a component's pagination or form-draft state into a file two screens can share.
  • Expose derived values as computed so no caller can write to them by accident.
  • Split a 300-line component's script into two composables with names that say what they do.

More examples

The composable behind it

One ref for state, one computed for the derived value, two functions for the changes. The caller cannot assign to `double`, which is the point of returning a computed. Because nothing here registers a lifecycle hook, the last four lines also run in a test with no component mounted.

Example · ts
// src/composables/useCounter.ts
import { computed, ref } from 'vue'

export function useCounter(initial = 0, step = 1) {
  const count = ref(initial)
  const double = computed(() => count.value * 2)

  function increment() {
    count.value += step
  }

  function reset() {
    count.value = initial
  }

  return { count, double, increment, reset }
}

const { count, double, increment } = useCounter(3, 5)
increment()
console.log(count.value, double.value)
// Output: 8 16

When the state is a reactive object

Returning `state` directly would work, but the first caller who writes `const { page } = usePagination(50)` gets a dead number. Spreading `toRefs(state)` turns each property into a ref that still reads and writes through the reactive object, so destructuring is safe.

Example · ts
// src/composables/usePagination.ts
import { computed, reactive, toRefs } from 'vue'

export function usePagination(total: number, perPage = 10) {
  const state = reactive({
    page: 1,
    perPage
  })

  const pageCount = computed(() => Math.ceil(total / state.perPage))

  function next() {
    if (state.page < pageCount.value) state.page += 1
  }

  // toRefs keeps every property reactive after the caller destructures
  return { ...toRefs(state), pageCount, next }
}

Returning a tuple when the caller will rename

An object return is the default because the names document themselves. A tuple is worth it for a one-or-two-value composable a component uses more than once, since array destructuring renames without the `: alias` noise. The two forms are equally reactive — both hand back refs.

Example · ts
// src/composables/useToggle.ts
import { ref, type Ref } from 'vue'

export function useToggle(
  initial = false
): [Ref<boolean>, (value?: boolean) => void] {
  const state = ref(initial)

  function toggle(value?: boolean) {
    state.value = typeof value === 'boolean' ? value : !state.value
  }

  return [state, toggle]
}

const [isMenuOpen, toggleMenu] = useToggle()
const [isModalOpen, toggleModal] = useToggle(true)
console.log(isMenuOpen.value, isModalOpen.value)
// Output: false true

Discussion

  • Be the first to comment on this lesson.