Extracting Logic into Composables: Core Concepts
Reuse stateful logic, not markup
A Vue composable is a plain JavaScript or TypeScript function that uses Composition API features and returns reactive state or operations. The conventional useX name signals that it participates in Vue's composition model. A composable can encapsulate refs, computed values, watchers, lifecycle hooks, and cleanup. It is useful when components share stateful behavior, such as fetching a resource, tracking pointer position, or synchronizing a preference.
A composable is not a component without a template. Components own rendered structure and user interaction; composables own reusable logic. If the repeated work is presentation or semantic markup, a component is often a better abstraction. If several views need the same state transitions and side effects, a composable can keep those rules consistent while allowing each view to render differently.
Call setup-aware APIs in setup
Composables that call useRoute, inject, onMounted, or other component-scoped APIs should be invoked synchronously during setup or another composable that runs in the active instance. Calling them in an arbitrary timer or after setup has completed loses the active component context. A composable can be called conditionally only when the call still occurs synchronously and the hook ordering remains understandable; avoid hiding lifecycle registration inside unpredictable branches.
Each invocation normally creates its own state. Refs declared inside useWindowWidth give each component an independent subscription. Module-level refs are shared across every caller and may leak state between server-rendered requests. Decide whether state is per component, per app, or global and place it accordingly. Shared application state may belong in Pinia or an app-provided service when it needs persistence, debugging, or explicit reset.
Return reactive values clearly
Return refs and functions in an object so consumers can choose what they need. When destructuring a returned ref, the local binding remains a ref; when destructuring a reactive object's primitive property, reactivity can be lost unless toRefs is used. Document whether returned values are writable or read-only. Expose the smallest API that supports the use case so consumers cannot violate internal invariants.
Keep names and behavior focused. useFetchResource may own loading, data, error, request cancellation, and refresh. It should not also decide how a page lays out those states or what business message to show. Return structured state and let the component render it. If the composable has side effects, make cleanup and error semantics part of its contract.
Test and compose
Composables can call other composables to build higher-level behavior. Keep dependencies explicit and avoid circular ownership. A pure transformation helper may be easier to test as an ordinary function; not every utility needs the composable convention. For component-scoped composables, test through a small host component or the team's established composable test helper so lifecycle hooks run in a valid scope.
Extract when reuse or complexity improves clarity, not solely to reduce line count. A composable used once can still be worthwhile if it isolates a complex state machine, but premature generalization creates options no caller needs. Keep the abstraction close to its owning feature until a second use case demonstrates the shared contract.
Extracting Logic into Composables: Practice
Extract a resource-loading flow
Two views load a project by id and repeat the same data, pending, error, refresh, and cancellation logic. Extract a useProject composable that accepts an id, returns a project ref, pending state, error state, and a refresh function. Keep the view responsible for labels, layout, and whether the error appears as an inline message or a page-level fallback.
The composable can watch the id and request the project when it changes. Use AbortController or a request generation token so a stale response cannot overwrite a newer project. Reset errors when a new request starts and define whether old data stays visible while loading. These are behavior decisions shared by both views and therefore belong in the composable contract.
Call useProject synchronously in setup. If it registers a watcher or lifecycle hook, Vue associates those effects with the component scope and stops them when the component is removed. If it adds an external event listener or timer, register cleanup with onScopeDispose. Return a stop or refresh method only if consumers need that control; otherwise keep lifecycle ownership internal.
Preserve reactivity at the boundary
Pass a ref or getter when the id can change. Passing the current primitive value captures only the value at setup time. For a simple example, a composable can accept a MaybeRefOrGetter and use toValue inside a watcher getter. Document the accepted shapes and TypeScript types so a caller does not accidentally make a dynamic id static.
Return refs for reactive state. If a consumer needs to destructure the result, it can destructure refs without losing reactivity. Avoid returning one mutable reactive object and expecting a primitive property to remain reactive after plain destructuring. Use readonly wrappers for data consumers should not mutate, and expose methods for supported state transitions.
Test behavior in a host component
Mount a minimal component that calls the composable with an initial id. Resolve a request and verify data and pending state. Change the prop or ref and verify a new request occurs. Resolve requests out of order and ensure only the current id updates the result. Unmount while a request is pending and verify cleanup prevents stale writes. Test network errors and retry behavior as well as the happy path.
If the composable is pure, test it without mounting. If it uses lifecycle APIs, use a test harness that creates and unmounts a component scope. Restore timers, mocks, and global listeners between tests. Do not test internal implementation details such as the exact number of watcher callbacks if the user-visible contract is what matters.
Avoid an oversized composable
Start with the smallest shared behavior. If useProject grows to include authorization, navigation, display formatting, caching, and toast messages, split responsibilities or return a lower-level state contract. Two views may share fetching but have different presentation and navigation behavior. Keep those differences at the component boundary so reuse does not become a bundle of flags and callbacks that is harder to understand than duplication.
Keep tests aligned with the public contract. If consumers rely on a readonly computed value, assert that it updates as inputs change rather than inspecting the internal ref name. This permits the composable to evolve without breaking callers.
Extracting Logic into Composables: Trade-offs and Interviews
Reuse has a boundary cost
A composable is valuable when it names a coherent stateful behavior and removes meaningful duplication. Extracting a one-line computed value into a generic composable can add indirection without helping. Conversely, a single-use composable can be justified when it isolates a complex lifecycle or async state machine. Evaluate clarity and ownership, not a rule that every repeated line must be abstracted.
Each composable invocation usually creates independent state. Shared state belongs at a deliberate scope. Moving refs to module level changes semantics for every caller and can cause cross-request leakage in SSR. Use a store or app-level service for intentionally shared state, with explicit reset and test isolation. Document whether a composable is per-instance, shared, or persistent.
Keep responsibilities explicit
Do not make a composable own presentation decisions, navigation policy, and data fetching unless these form a coherent contract. Avoid a large configuration object with many booleans for hypothetical consumers. Expose typed inputs and a small return surface. Return readonly refs when mutation would violate invariants, and provide explicit methods for supported changes.
Composable tests need a valid Vue scope when lifecycle APIs are used. This is a testability consideration, not a reason to avoid composables. Keep pure transformations outside the lifecycle-bound function when possible. A small host component can verify setup, reactive inputs, cleanup, and async behavior with less mocking than tests that simulate internals.
In an interview, define a composable as reusable stateful logic, distinguish it from a component, and explain setup-time invocation and per-call state. Give an example of a reactive parameter and cleanup path. Finish with when you would use a store instead of module-scoped refs. This shows the abstraction's state and lifetime semantics are understood, not just its naming convention.
Avoid hidden coupling through module globals, implicit router access, or app-specific assumptions. Pass dependencies such as an API client or clock when that makes the behavior easier to test and reuse. Do not add dependency injection solely for a theoretical future; a composable can import a stable application service when ownership is clear. The useful boundary is the one that separates reusable behavior from view-specific decisions.
Composables should have predictable cleanup. If a function adds a window listener, timer, or watcher tied to an effect scope, dispose it when that scope ends. If it returns an explicit stop method, define whether calling stop prevents future restart or whether a separate start method exists. Avoid APIs that let two components accidentally stop a shared resource they do not own.
Use names that describe behavior rather than implementation detail. useOnlineStatus communicates a capability; useRef1 does not. Keep return values cohesive and typed. If one composable exposes dozens of unrelated refs, split it by lifecycle or domain boundary. Conversely, do not split tightly coupled state transitions into helpers that make invariants impossible to see together.
In an interview, explain a composable you extracted, why a component was not the right reuse unit, how each call's state is scoped, and how cleanup occurs. Discuss how you test reactive changes and errors. Mention the point where shared state belongs in a store. This demonstrates design judgment beyond knowing the use prefix.
Quiz
1. Which statement about Extracting Logic into Composables is accurate?
2. When reviewing Extracting Logic into Composables, which point must hold?
3. What is a Vue composable primarily intended to reuse?
4. When should a composable using lifecycle hooks normally be called?
Flashcards
Question
What is Extracting Logic into Composables?
Click to reveal answer
Answer
Refactor component state and effects into reusable useX functions.
Question
Extracting Logic into Composables: why it matters
Click to reveal answer
Answer
A composable is a plain function that uses ref and lifecycle hooks and returns reactive state.
Question
Extracting Logic into Composables: key detail
Click to reveal answer
Answer
The composable naming convention prefixes functions with use to signal composition.
Question
Extracting Logic into Composables: practical note
Click to reveal answer
Answer
Because composables call setup-time APIs, they should be invoked during component setup.
Question
What is a Vue composable primarily intended to reuse?
Click to reveal answer
Answer
Composables encapsulate reusable reactive state and behavior; components remain responsible for rendering.
Revision Notes
Key Takeaways
- 1. A composable is a plain function that uses ref and lifecycle hooks and returns reactive state.
- 2. The composable naming convention prefixes functions with use to signal composition.
- 3. Because composables call setup-time APIs, they should be invoked during component setup.
Interview Tips
- • Explain Extracting Logic into Composables with one concrete example from real vue work, not a textbook restatement.
- • Discuss the trade-off: when Extracting Logic into Composables helps and when a lighter approach wins.
- • Place Extracting Logic into Composables inside the Lifecycles and Composables phase — interviewers check that you see the progression, not isolated facts.
Cheat Sheet
Extracting Logic into Composables Cheat Sheet
Phase: Lifecycles and Composables | Level: intermediate
Key points:
- A composable is a plain function that uses ref and lifecycle hooks and returns reactive state.
- The composable naming convention prefixes functions with use to signal composition.
- Because composables call setup-time APIs, they should be invoked during component setup.
- Extract a composable when two or more components repeat the same stateful logic.
Tags: vue, composables, patterns
Continue Learning
Before You Start
- No prerequisites listed.