Skip to main content

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.

  1. Cache Lookup: When called, useFetch first checks the DataClient cache using the provided key.
  2. Suspense & Fetching: If data is missing or expired, it initiates the fetch action. If suspense: true (default), it throws a Promise, causing React to suspend the component until data arrives.
  3. Reactivity: It uses useSyncExternalStore to 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.
  4. Request Deduplication: If multiple components request the same key simultaneously, useFetch deduplicates 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, and enduring options 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) and isFetching (background refetch) for precise UI feedback.
  • Lifecycle Hooks: Offers before and after hooks 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.
Automatic vs. explicit

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 Loadable component with a useSuspenseFetch inside 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: Loadable supports prefetching and preloading its chunk via render options, cutting the first leg of the waterfall for likely-needed components.
  • Pair with useIsVisible for 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:


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 useSuspenseFetch when a component cannot render without its data (non-null data, no isLoading/isError branches)
  • Configure suspense and suspendAfterFirstLoad for optimal UX
  • Rely on structural sharing to avoid re-renders on structurally identical refetch results
  • Set appropriate ttl based on data volatility
  • Reserve enduring for critical global data to avoid memory leaks