A Reusable Data-Fetching Composable: Core Concepts
Encapsulate request lifecycle
A data-fetching composable can expose data, pending, error, and refresh while owning request cancellation and stale-response protection. It reduces repeated request state logic across views, but it should not become an unbounded HTTP framework. Keep transport configuration in an API client and let the composable own the reactive lifecycle for one resource or query.
If the URL or id is reactive, watch that source and refetch when it changes. Accept a ref, getter, or explicit refresh input when callers need different reactivity. Reading the value once at setup gives a snapshot, so a route parameter change will not automatically fetch unless the source is tracked. Watch the narrowest key that changes the request rather than the entire route object.
Prevent races and clean up
When a new request starts, abort the previous request where supported and associate a generation id with the active request. Only the current generation may commit data or clear pending state. An older finally block must not turn off a newer loading indicator. Aborting saves client work but does not prove a server-side write was rolled back.
Register watcher cleanup synchronously before awaiting. If the component unmounts, Vue stops its setup-created watcher and cleanup can abort its request. If a timer, event listener, or subscription is created, release it with the effect scope. Return a refresh function that uses current inputs and define whether it bypasses debounce or cache.
Define error and cache boundaries
The composable should check HTTP status and parsing errors, then expose a stable error category. It should distinguish intentional abort from network failure. Decide whether prior data remains visible on refresh and whether the composable caches results. If several components need request deduplication, invalidation, background refresh, and retries, a query library is usually more appropriate than a hand-built cache.
Avoid hidden global state inside a composable module. Per-call refs are isolated; module-scoped data is shared and can leak across SSR requests. If the composable intentionally shares a cache, key it by all request inputs and user scope, define freshness and invalidation, and create request-scoped state on the server.
Test the public contract
Test initial load, reactive URL changes, refresh, failure, cancellation, and out-of-order responses. Use a host component for lifecycle behavior. Confirm unmount stops the request and that an old response cannot replace current data. Test different consumers independently unless shared caching is the declared contract. The composable is useful when it makes async state consistent while leaving presentation and domain decisions with the view or service.
A composable can receive a request function as well as a reactive key, which makes it easier to test and avoids constructing URLs in multiple components. Keep credentials and headers inside an API client or injected service, not in query strings or debug logs. If callers need different transformations, return the raw typed response and let a computed value derive presentation data.
Give each request a stable key that includes identity and filters, and document it for callers.
A Reusable Data-Fetching Composable: Practice
Implement latest-request-wins behavior
Create useResource(source) where source can be a string, ref, or getter. Return data, pending, error, and refresh. Watch the normalized URL immediately. For every run, create an AbortController and register watcher cleanup before awaiting. Clear the current error, set pending, and capture a request generation. When a result arrives, commit it only if its generation is still active. In finally, clear pending only for that same generation.
Check response.ok before decoding JSON. Map non-success status into an error with a safe code. Treat AbortError from an obsolete request as cancellation rather than a visible error. If the URL is empty or invalid, set a validation state and do not send a request. Do not expose the raw response body or request headers in the error object returned to a component.
Define refresh and stale-data behavior
Refresh should use the current source and clear or preserve data according to the component contract. If old data remains while loading, expose a refreshing indicator so the view can label it as potentially stale. If it is cleared, avoid a jarring layout reset. A composable can expose isRefreshing separately from initial pending if consumers need different UI.
If the request is a read, retry may be safe with bounded backoff. If the composable also supports mutations, keep them in explicit actions with idempotency and operation-status handling rather than using a generic refetch retry. A timeout on a write does not reveal whether the server committed. Keep read and write semantics distinct.
Test reactive input forms
Pass a fixed URL and verify the initial load. Pass a ref and change it; verify a second request occurs and the first is aborted. Pass a getter based on a prop and test the parent update. Resolve responses out of order and ensure only the latest result appears. Unmount during a pending request and confirm cleanup runs without an unhandled rejection.
Test malformed JSON, 404, 500, offline rejection, abort, and successful empty data. Verify the error state differentiates a successful empty response from failure. Test refresh after failure and confirm the old error clears. If the composable uses an internal cache, test key isolation across query params, tenant, and authenticated identity.
Know when to switch to a query library
Once the feature needs shared cache, request deduplication, background refetch, invalidation after mutations, optimistic updates, retries, or hydration, compare a proven query library such as TanStack Query for Vue. A small composable is easy to understand for one endpoint; reproducing cache lifecycle for many resources creates subtle bugs. Avoid using both a hand-written cache and a query cache as competing sources of truth.
Keep the composable API small and typed. Its success is measured by consistent request lifecycle and safe cancellation, not by the number of options it exposes. Components should be able to render the states without knowing internal generation counters or controller bookkeeping.
Register cleanup synchronously inside the watcher callback before awaiting. If a source changes while the request is pending, Vue can run the cleanup before starting the next request. Keep the current generation id in a local closure so an old finally branch cannot clear new pending state.
A Reusable Data-Fetching Composable: Trade-offs and Interviews
Reuse can become a hidden cache
A composable that exposes request state is a useful boundary for one resource. If it adds module-level caching, deduplication, retries, invalidation, and persistence, it becomes a query system and needs corresponding correctness guarantees. Cache keys must include every input that affects the response, including tenant or user scope. Shared server-side cache state must not cross SSR requests.
Reactive inputs must be read inside a tracked getter. Passing a plain value is a snapshot; a ref or getter is needed for later changes. Watch the specific request key and avoid refetching on unrelated route hash or query changes. Use cancellation and request identity because abort may not be supported and responses can race.
Cancellation is not rollback
AbortController can stop client-side waiting and sometimes network transfer, but the server may already have processed the request. It is suitable for superseded reads; writes need idempotency and a status or reconciliation strategy. Do not show a canceled obsolete read as a failure. Do show a timeout as a recoverable error when the current user action remains unresolved.
If several views share the same resource and need stale-while-revalidate, focus refetching, query invalidation, or optimistic updates, use a dedicated query library rather than adding options to a small composable. Keep server cache, Pinia domain state, and component state distinct so there is one authoritative owner for each value.
In an interview, explain reactive source tracking, latest-request-wins behavior, error mapping, and cleanup. Describe the boundary where you would adopt a query library and why. Mention that client cancellation cannot undo server writes and that cache keys must reflect identity. This frames a fetching composable as a lifecycle abstraction, not a replacement for HTTP semantics.
Request identity should include all parameters, not only the URL string when headers or authenticated identity change the response. On logout, cancel private requests and clear user-scoped data. If a composable is used during SSR, create request-scoped dependencies and avoid module-level mutable caches.
Automatic retries need an explicit policy. Retry only error classes that are plausibly transient, cap attempts, and use backoff. Do not retry authorization failures or invalid requests unchanged. A query library can provide retry and cache policy consistently across endpoints, while a small composable can remain deliberately simple for one isolated resource.
Expose cancellation and pending state as part of the behavior contract. A component may need to distinguish initial load from refresh and show old data while the active request changes. Keep the composable's return shape stable and type errors sufficiently for the view to render safe messages. Avoid returning raw Response objects that have already been consumed or contain implementation-specific behavior.
A keyed request contract makes cache isolation testable. This also prevents stale data from crossing user sessions.
A composable is most useful when it gives callers a small, stable contract: what input it accepts, which refs it returns, when it fetches, and how the caller can cancel or refresh. Keep HTTP status handling, parsing, and request cancellation inside a clear boundary, but leave screen-specific wording and navigation to the component. If the caller changes an ID while a request is pending, abort the old request where possible and guard against a late response overwriting the new result. Avoid a module-level mutable result unless shared caching is an explicit design; otherwise two component instances can unexpectedly overwrite each other's state.
Quiz
1. Which statement about A Reusable Data-Fetching Composable is accurate?
2. When reviewing A Reusable Data-Fetching Composable, which point must hold?
3. How should a fetching composable prevent an older response from replacing newer data?
4. When is a query library preferable to a small custom fetching composable?
Flashcards
Question
What is A Reusable Data-Fetching Composable?
Click to reveal answer
Answer
Wrap fetch, cancellation, and state into a useResource-style function.
Question
A Reusable Data-Fetching Composable: why it matters
Click to reveal answer
Answer
A composable accepting a reactive URL ref refetches automatically when the URL changes.
Question
A Reusable Data-Fetching Composable: key detail
Click to reveal answer
Answer
AbortController cancels stale in-flight requests when a newer one starts.
Question
A Reusable Data-Fetching Composable: practical note
Click to reveal answer
Answer
Race conditions appear when an old response resolves after a new one, which abort prevents.
Question
How should a fetching composable prevent an older response from replacing newer data?
Click to reveal answer
Answer
Cancellation or an active request identity prevents stale responses from committing after inputs change.
Revision Notes
Key Takeaways
- 1. A composable accepting a reactive URL ref refetches automatically when the URL changes.
- 2. AbortController cancels stale in-flight requests when a newer one starts.
- 3. Race conditions appear when an old response resolves after a new one, which abort prevents.
Interview Tips
- • Explain A Reusable Data-Fetching Composable with one concrete example from real vue work, not a textbook restatement.
- • Discuss the trade-off: when A Reusable Data-Fetching Composable helps and when a lighter approach wins.
- • Place A Reusable Data-Fetching Composable inside the HTTP Clients and Data Fetching phase — interviewers check that you see the progression, not isolated facts.
Cheat Sheet
A Reusable Data-Fetching Composable Cheat Sheet
Phase: HTTP Clients and Data Fetching | Level: advanced
Key points:
- A composable accepting a reactive URL ref refetches automatically when the URL changes.
- AbortController cancels stale in-flight requests when a newer one starts.
- Race conditions appear when an old response resolves after a new one, which abort prevents.
- Returning data, error, loading, and refresh gives components a complete request interface.
Tags: vue, composables, http
Continue Learning
Before You Start
- No prerequisites listed.