skip to content

Composition API

Composition-style code runs setup logic once per instance and extracts it into composables, passes values down with provide/inject or shares module state. Interviewers compare it with hooks.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

18

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.
open as a page

In a Vue 3 app, how do provide() and inject() get a layout's theme to deeply nested components without passing props?

level: juniorimportance: must knowfreq 65%

basics

~20 s

The layout calls provide('theme', theme) in its setup; any descendant, however deep, calls inject('theme') in its setup to receive it. Components in between pass nothing, and only the provider's subtree can inject it; app.provide() makes a value available app-wide.

open as a page

In Vue 3, what arguments does a component's setup() function receive, and what is it allowed to return?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Vue 3's setup(props, context) receives reactive, read-only props and a context with attrs, slots, emit and expose; it runs once per instance with no this, and returns an object of template bindings or a render function.

open as a page

Why does Vue 3 recommend composables over mixins and renderless components for reusing stateful logic between components?

level: middleimportance: must knowfreq 62%

basics

~20 s

Composables make every reused name explicit at the call site, resolve collisions by renaming and pass data between units as arguments, fixing mixins' three flaws, and unlike renderless components they add no extra component instance.

open as a page

In Vue 3, why does an injected locale stop updating when the provider passes locale.value, and how should injectors change it?

level: middleimportance: must knowfreq 55%

basics

~20 s

provide('locale', locale.value) provides a plain string captured once, so later changes never reach injectors. Provide the ref itself, ideally wrapped in readonly(), plus a setLocale function, so injectors stay reactive and all changes happen in the provider.

open as a page

In Vue 3, how can distant components share a notification queue without any store library?

level: juniorimportance: should knowfreq 55%

basics

~20 s

Create the queue with reactive() or ref() at the top level of a plain module and import it where needed. Module scope makes it a singleton, and Vue tracks reactive reads anywhere, so every component reading it updates.

open as a page

How would you write a Vue 3 useFetch(url) composable that accepts a string, a ref or a getter and re-fetches when the URL changes?

level: middleimportance: should knowfreq 55%

basics

~20 s

Type url as MaybeRefOrGetter<string>, call toValue(url) inside a watchEffect (or as a watch source) so the ref or getter is tracked, fetch into data and error refs, abort stale requests in cleanup, and return the refs.

open as a page

How does a Vue 3 composable differ from a React custom hook in when its code runs and how it tracks dependencies?

level: middleimportance: should knowfreq 52%

basics

~20 s

A Vue composable runs once per component, during setup, and its computed values and watchers track dependencies automatically; a React custom hook re-runs on every render and relies on call order and explicit dependency arrays.

open as a page

In Vue 3, what does inject() return when no ancestor provides the key, and how do plain and factory defaults change that?

level: middleimportance: should knowfreq 40%

basics

~20 s

With no provider and no default, inject() returns undefined and development builds warn that the injection was not found. A second argument supplies a default; with true as the third, a function default is called as a factory.

open as a page

How do you write a Vue 3 component without an SFC whose setup() returns a render function, and still let a parent call its methods?

level: middleimportance: should knowfreq 35%

basics

~10 s

Define the component with defineComponent, return a render function built with h() from setup(), and call expose({ method }) from the setup context; a parent's template ref then reaches exactly the exposed members.

open as a page

How does Vue 3's <script setup> relate to a setup() function, and when would you still write setup() explicitly?

level: middleimportance: should knowfreq 48%

basics

~20 s
<script setup> compiles into the component's setup(): top-level bindings reach the template directly and the instance is closed to parent refs by default. Write setup() explicitly without an SFC build step or for render-function components.
open as a page

In a Vue 3 module-level store, how do you stop components mutating state directly, and what does readonly() actually enforce?

level: middleimportance: should knowfreq 42%

basics

~20 s

Keep the reactive state private to the module, export readonly(state) plus action functions such as push and dismiss. readonly() gives a deep proxy whose writes are ignored with a development-only warning; it guides callers but is not a hard security boundary.

open as a page

In Vue 3, what changes when a useNotifications() composable creates its ref at module scope instead of inside the function?

level: middleimportance: should knowfreq 45%

basics

~20 s

A ref created inside the composable is new on every call, so each component gets private state; a ref created at module scope is created once and shared by every caller, turning the composable into a singleton store.

open as a page

A Vue 3 composable that registers onUnmounted and a watcher works at the top of setup but leaks when called from a click handler — why?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Vue attaches lifecycle hooks and watchers to the component whose setup is currently running; in a click handler there is no active instance, so onUnmounted is dropped with a dev warning and the watcher is never stopped automatically.

open as a page

How would you build a Vue 3 form context so nested field components register with their form through provide() and inject()?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The Form provides, under an exported Symbol key, a context of readonly state plus register and unregister functions. Each field injects it in setup, registers itself, and unregisters in onUnmounted; the Form validates and submits through its registry.

open as a page

What changes when a Vue 3 component's setup() is async, and why do lifecycle hooks registered after its first await fail?

level: seniorimportance: should knowfreq 36%

basics

~20 s

An async setup() returns a promise, so the component renders only under a Suspense ancestor; and because Vue's current instance is cleared at the first await, hooks, watchers and inject() calls after it are not bound to the component.

open as a page

In a Vue 3 SSR app, why does a module-level reactive() notification store leak one user's messages to another, and how must it change?

level: seniorimportance: should knowfreq 45%

basics

~20 s

On a server, modules are initialised once at boot and reused by every request, so a module-level reactive() store is shared by all users. Vue calls this cross-request state pollution; create the store per request in the app factory and provide it.

open as a page

When should a Vue 3 team stop hand-rolling module-level reactive stores and adopt a store library?

level: principalimportance: should knowfreq 38%

basics

~20 s

Adopt a store library when shared state needs things the hand-rolled pattern lacks: team conventions, devtools inspection and time travel, hot reload that keeps state, and per-request SSR isolation. Until then, module stores with readonly views and actions are enough.

open as a page