useFetch: Fetch + Suspense + Error Boundaries + Lazy Loading
Comprehensive Best Practices Guide
Introduction
This guide provides a detailed overview of best practices when combining Archibald's useFetch hook with modern React features like Suspense, Error Boundaries, and lazy-loaded components via Loadable.
How it works
Under the hood, useFetch acts as a smart, reactive bridge between your React components and Archibald's central DataClient.
- Cache Lookup: When called,
useFetchfirst checks theDataClientcache using the provided key. - Suspense & Fetching: If data is missing or expired, it initiates the fetch
action. Ifsuspense: true(default), it throws a Promise, causing React to suspend the component until data arrives. - Reactivity: It uses
useSyncExternalStoreto subscribe to the specific cache entry. This ensures that if the data is updated elsewhere (e.g., by a mutation or another fetch), the component re-renders automatically with the fresh data. - Request Deduplication: If multiple components request the same key simultaneously,
useFetchdeduplicates these calls, ensuring only one network request is made.
Why a Custom Solution (vs. TanStack Query)?
Archibald's useFetch was developed as a custom solution rather than adopting an existing library like TanStack Query for several strategic reasons:
- Seamless Integration: To ensure deep and consistent integration with Archibald's core architecture, including its
DataClient,AppClient, configuration management, and routing system. This allows for a highly cohesive developer experience. - Tailored Control: To provide precise control over caching, SSR, and Suspense behaviors that are specifically optimized for Archibald's framework design and performance goals.
- Optimized Bundle Size: To maintain a lean client-side bundle by including only the necessary data fetching and caching logic, avoiding the overhead of features not critical to Archibald's ecosystem.
- Opinionated Design: To align with Archibald's opinionated approach to building robust and scalable applications, offering a standardized way to handle asynchronous data that fits naturally within the framework's paradigms.
Advantages of Archibald's useFetch
- Native Integration: Works out-of-the-box with Archibald's state management, routing, and configuration.
- Modern React Alignment: First-class support for React Suspense and Error Boundaries, promoting declarative data fetching and robust UI.
- Predictable Caching: Granular control over cache keys,
ttl, andenduringoptions for efficient data management. - SSR-Ready: Designed for optimal performance with Server-Side Rendering, reducing client-side waterfalls.
- Clear Loading States: Distinguishes between
isLoading(initial fetch) andisFetching(background refetch) for precise UI feedback. - Lifecycle Hooks: Offers
beforeandafterhooks directly within the action parameter for localized side effects.
Disadvantages of Archibald's useFetch
- Learning Curve: Developers new to Archibald may need to familiarize themselves with its specific conventions and API for data fetching.
- Framework Coupling: Tightly coupled with Archibald, which could present challenges if migrating away from the framework.
Best practices
Prefer useSuspenseFetch when the component needs its data
If a component cannot render anything meaningful without its data, reach for useSuspenseFetch. It always suspends and always throws errors to the nearest error boundary, so data is guaranteed non-null and you can drop the isLoading / isError branches. Keep plain useFetch for data that is genuinely optional or where you want to render a local loading/empty state inline.
// Needs the product to render → suspense-first, no null checks.
function ProductTitle({ id }: { id: string }) {
const { data } = useSuspenseFetch(['product', id], () => api.getProduct(id));
return <h1>{data.name}</h1>;
}
// Optional side panel → plain useFetch with an inline branch.
function Recommendations({ id }: { id: string }) {
const { data, isLoading } = useFetch(['recommendations', id], () => api.getRecommendations(id), { suspense: false });
if (isLoading) return <Skeleton />;
return <List items={data ?? []} />;
}
Cancel in-flight requests with the run signal
Every fetch run receives an AbortSignal on its action params. It aborts automatically when the run is superseded by a newer one for the same key (a refetch, poll tick, or focus refetch), when the entry is deleted, or when the client is destroyed. Forward it into your request to cancel the actual network call and avoid wasted work and race conditions — a run that rejects because its signal aborted is treated as cancelled, not an error, so it never trips an error boundary or clobbers previous data.
const { data } = useFetch(['search', term], ({ signal }) => api.search(term, { signal }));
// Typing quickly supersedes each run; the stale request is aborted instead of racing the newest one.
Cancellation is wired through the run lifecycle automatically, but the network call is only aborted if your action forwards signal into the request (api.createRequest({ url, signal }) / fetch(url, { signal })). Actions that ignore it still get correct cache semantics, just no early network abort.
Refetch on focus and polling
For data that goes stale while the tab is backgrounded, opt into refetchAfterFocus; for data that changes server-side on its own, use poll. Both re-run through the same request path (and supersede/cancel the previous run), and both are cleaned up automatically when the component unmounts.
// Revalidate when the user returns to the tab.
useFetch(['cart', cartId], () => api.getCart(cartId), { refetchAfterFocus: true });
// Poll a status endpoint every 5s.
useFetch(['order-status', orderId], () => api.getOrderStatus(orderId), { poll: 5000 });
Keep data fresh after a mutation
Don't clear-and-refetch after a write. Call client.invalidate(key) — it revalidates stale-while-revalidate (without clearing first), so useFetch / useSuspenseFetch consumers keep their current content on screen instead of flashing a Suspense fallback. See the useMutation best practices.
Lazy loading + data fetching
Code-split heavy components with Loadable — Archibald's isomorphic lazy-loading wrapper that code-splits on both server and client (page and component level). When a lazy component also fetches data, watch out for waterfalls: the chunk downloads first, and only then does the component mount and start its fetch.
- Share one Suspense boundary: A
Loadablecomponent with auseSuspenseFetchinside can resolve chunk and data under the same boundary — one fallback, no double flicker. - Start fetching before the chunk lands: For critical data, kick off the fetch in the parent (or via a route-level prefetch) so the network request runs in parallel with the chunk download, instead of after it.
- Use
prefetched/preloaded:Loadablesupports prefetching and preloading its chunk via render options, cutting the first leg of the waterfall for likely-needed components. - Pair with
useIsVisiblefor below-the-fold content: Only mount the lazy component (and thereby trigger its fetch) when it approaches the viewport.
See Also
For a detailed technical breakdown and additional implementation patterns, refer to the following resources:
- API References
- useSuspenseFetch — the Suspense-first variant with guaranteed non-null
data - useMutation best practices — writes, transitions and post-mutation revalidation
- Structural sharing — referential stability of
dataacross refetches - Common Patterns
Key Takeaways
- Use a centralized fetch wrapper
- Follow Archibald Fetch best practices
- Wrap async operations with Suspense and Error Boundaries
- Use nested fallbacks strategically to avoid flickers
- Plan lazy loading + data fetching to minimize waterfalls
- Understand the loading and error resolution hierarchy
- Reach for
useSuspenseFetchwhen a component cannot render without its data (non-nulldata, noisLoading/isErrorbranches) - Configure
suspenseandsuspendAfterFirstLoadfor optimal UX - Rely on structural sharing to avoid re-renders on structurally identical refetch results
- Set appropriate
ttlbased on data volatility - Reserve
enduringfor critical global data to avoid memory leaks