skip to content

In Vue 3, what is a composable, and what separates it from an ordinary utility function such as a date formatter?

level: juniorimportance: must knowfreq 68%

answer

  1. stateful versus stateless logic
  2. Composition API inside a function
  3. useX naming convention
  4. new state on every call
  5. plain object of refs out

basics

~10 s

A composable is a function, named useX by convention, that uses Vue's Composition API to encapsulate stateful logic: it creates its own refs, can hook into the calling component's lifecycle, and returns that state.

solid answer

~40 s

A date formatter is **stateless**: input in, output out, nothing kept between calls. A composable wraps **stateful** logic with the Composition API. `useMouse()`, for example, creates `x` and `y` with `ref()`, adds a listener in `onMounted()`, removes it in `onUnmounted()`, and returns `{ x, y }`. Because the refs are created inside the function, every component that calls it gets its own copies. Composables can call other composables, which is where the name Composition API comes from. The conventions are a camelCase `use` prefix, a plain object of refs as the return value so callers can destructure it, and a synchronous call from `setup()` or `<script setup>`.

code

ts · 16 lines
ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

go deeper

for a junior

Recall the one-line definition: a useX function that uses ref, watchers and lifecycle hooks to package stateful logic, and returns its state for the caller.

for a middle

Explain why each call gets fresh state, why the return value is a plain object of refs, and how nested composables register hooks on the caller's instance.

for a senior

Show judgment about when extraction pays off: composables for reuse and for splitting a large component by concern, with explicit inputs and outputs rather than hidden coupling.

for a principal

Frame composables as the team's unit of logic reuse: naming, input and return conventions, and cleanup guarantees a shared library of them must follow.

## Stateless logic versus stateful logic Most reuse in a frontend codebase starts with plain functions. A date formatter takes an input and returns an output immediately. It holds nothing between calls, so any module can import it and nothing about Vue matters. That is **stateless logic**, and ordinary utility libraries already cover it. **Stateful logic** is different. It manages values that change over time and usually touches the outside world: the current mouse position, whether the browser is online, the result of a request that is still loading. In Vue 3 that kind of logic needs reactive state, so that templates update, and often side effects that must start when a component appears and stop when it goes away. A **composable** is the unit the Vue docs define for it: a function that uses the Composition API to encapsulate and reuse stateful logic. ## Anatomy of a composable A typical composable does four things, in this order: 1. **Creates its own reactive state** with `ref()` (or `shallowRef()`, `computed()`) inside the function body. 2. **Sets up side effects** such as watchers, event listeners or timers that update that state. 3. **Hooks into the calling component's lifecycle**, for example `onMounted()` to start a listener and `onUnmounted()` to remove it, so nothing outlives the component. 4. **Returns the state** a caller needs, conventionally as a plain object of refs, plus any functions that act on it. Because the body runs when a component calls it during `setup()` (or at the top level of `<script setup>`), the lifecycle hooks and watchers it registers attach to that component instance. ## The conventions and why they exist | Convention | Reason | |---|---| | camelCase name starting with `use` | readers recognise a stateful, setup-time function at a glance; Vue itself does nothing with the prefix | | return a plain object of refs | the caller can destructure (`const { x, y } = useMouse()`) and each variable stays reactive | | accept refs or getters as input | the composable can react to changing arguments, not only the value at call time | | clean up what you start | listeners and timers are removed in `onUnmounted()`, so an unmounted component leaks nothing | | call it synchronously from setup | only then can Vue tie hooks and watchers to the right component | Returning a `reactive()` object instead breaks the destructuring pattern: pulling a property out of a reactive object gives a plain value that is disconnected from the source. A caller who prefers `mouse.x` access can wrap the result itself with `reactive(useMouse())`, which unwraps the refs while keeping the link to the composable's state. ## Each call gets its own state The refs are created inside the function, so every call creates new ones. Two components that both call `useMouse()` each get their own `x` and `y`, and neither sees the other's values. This is the key behavioural difference from a module that creates state at import time. Sharing state across components requires deliberately moving it outside the function, which is a separate pattern with its own trade-offs. ## Composables compose A composable may call other composables. `useMouse()` can delegate listener management to a smaller `useEventListener(target, event, handler)` that adds the listener in `onMounted()` and removes it in `onUnmounted()`. The inner composable's hooks register against the same component, because the whole call chain runs synchronously inside that component's setup. This nesting is where the Composition API gets its name: complex behaviour is assembled from small, isolated functions, the way an application is assembled from components. Composables are also a code-organisation tool, not only a reuse tool. A large component can be split into `useFeatureA()` and `useFeatureB(foo)`, where one composable's returned ref is passed to the next as an argument. The data flow between concerns becomes explicit instead of living in shared properties. ## What a composable is not - It is **not a component**: it renders nothing and creates no component instance of its own. - It is **not re-run on every update**: its body runs once per call, during setup; afterwards only the watchers and computed values it created respond to changes. - It is **not a singleton**, unless its author deliberately hoists state out of the function. - It is **not special to the compiler**: the `use` name is a convention; what matters is which Vue APIs it calls and where it is called from. A good one-line definition for an interview: a composable is a setup-time function that owns a slice of reactive state and its side effects, and hands the state back to the component that called it.

  • Why do Vue composables conventionally return a plain object of refs rather than a reactive() object?
    Destructuring a `reactive()` object copies out plain values, so the caller loses the connection to the composable's state. Refs survive destructuring because each variable still holds the ref object. A caller who wants property-style access can wrap the result: `const mouse = reactive(useMouse())` unwraps the refs and keeps them linked.
  • If two components each call useMouse(), do they share x and y?
    No. `ref(0)` runs inside the function body, so each call creates new refs, and each component tracks its own values and registers its own listener. Sharing requires creating the state outside the function, at module scope, which is a deliberate design choice with its own caveats, not the default behaviour of a composable.
  • Can a composable call another composable, and whose lifecycle do the inner hooks attach to?
    Yes. `useMouse()` can call `useEventListener(window, 'mousemove', handler)`. The whole chain runs synchronously inside the calling component's setup, so the inner `onMounted()` and `onUnmounted()` register on that same component instance.

A composable is a recipe card, not a shared pot: every cook who follows the card bakes a separate cake with their own ingredients. Sharing one cake means deliberately keeping it outside the recipe.

saying these in an interview costs you the question

  • Any exported helper function, even a pure formatter, counts as a composable.
  • Components that call the same composable share one copy of its state.
  • A composable should return a reactive() object so callers can destructure it.
  • Vue treats functions named useX specially at compile time.
  • A composable's body re-runs every time the component re-renders.