Data Fetching: Common Patterns & Standards
Archibald Fetch Interactions and Standards
Fetch Function Example
function Todos() {
const todosFetch = useFetch('todos', () => actionGetTodos());
return (
<ul>
{
todosFetch.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))
}
</ul>
);
}
Fetch Keys
Fetched data for every unique fetch key will be cached in DataClient. You can inspect currently cached values in browser console with window.__DATA_CLIENT__.dataCache.values in development mode only
useFetch keys can be of type string, number, null. or an array of these types.
import { useFetch } from '@archibald/client';
const RETURN_VALUE = useFetch<DATA_TYPE>(PARAMETERS);
Mutations with Cache Updates
To update values in Dataclient cache, use useMutation with the same key.
function Todos() {
const todosFetch = useFetch('todos', () => actionGetTodos());
const { mutate } = useMutation('todos');
function addTodo(){
mutate(actionAddTodo())
}
return (
<>
<button onClick={addTodo}>Add todo</button>
<ul>
{todosFetch.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
);
}
Why
- Consistency avoids cache collisions
- Typed fetch functions improve TypeScript safety
- Mutations that update cache reduce unnecessary refetches
Caching & Data Client
Cache Keys
The fetchKey is critical for caching. It acts as a dependency array for the cache.
- String Key:
'users' - Array Key:
['users', userId, { filter: 'active' }]
The Data Client uses these keys to serialize the request and store the result. If any part of the key changes, a new fetch is triggered (unless enabled is false).
Cache Lifetime & Persistence
ttl(Time To Live): Defines how long (in ms) data remains "fresh" in the cache. Defaults to 15 minutes. After this time, the data is considered stale and may be refetched or garbage collected.enduring: If set totrue, the cache entry is protected from automatic garbage collection. This is useful for critical data that must remain available (e.g., user session info), but should be used sparingly to avoid memory leaks.
Reactivity with useSyncExternalStore
The Data Client cache is external to the React tree. To ensure your component updates immediately when the cache is updated (e.g., by another component or a mutation), useFetch utilizes useSyncExternalStore. This ensures tearing does not occur and the UI is always consistent with the cache.
Server-Side Rendering (SSR)
useFetch is designed to work seamlessly with SSR.
- Execution: The
fetch data functionis executed on the server during the render pass. - Dehydration: The result is stored in the Data Client and dehydrated (sent to the client as JSON).
- Hydration: On the client,
useFetchinitializes with this data, avoiding a double fetch on mount.
Important: Ensure your fetch data function is isomorphic (runs on both server and client). Avoid accessing window or document directly in the fetch function.
Suspense + Fetch Behavior
What Suspense Does
- Suspense allows React to wait for async operations before rendering
- Archibald Fetch supports this via
suspense: true
When a component is wrapped in Suspense, React expects that the component might not be ready yet because it's waiting on some async data.
If the component throws a Promise, React shows the fallback UI until the Promise resolves. useFetch supports this pattern by throwing:
- a Promise (when loading)
- an Error (when the fetch fails)
So, Archibald useFetch plugs directly into React's Suspense architecture.
Loading Behavior
useFetch(
FETCH_KEYS.USERS, // key
() => actionGetUsers(), // action
{ suspense: true } // options
);
What happens during loading?
- Throws a Promise while data is loading
- The nearest Suspense boundary catches it
- The fallback is displayed
Why
- Promotes declarative loading states
- Reduces repetitive
isLoadinglogic - Enables nested progressive loading
Because throwing a Promise is how Suspense tells React: "Pause rendering this component; show the Suspense fallback instead"
Why the Suspense Fallback appears
When Archibald useFetch throws a Promise, React catches it and renders the fallback.
Example:
<Suspense fallback={<LoadingSpinner/>}>
<UserList />
</Suspense>;
Here's the sequence:
- UserList calls useFetch
- useFetch has suspense: true
- Data is not yet available → useFetch throws a Promise
- React Suspense catches it → shows
LoadingSpinner - When the Promise resolves → React re-renders
UsersListwith real data
Error Boundary Behavior
Centralized Error Handling
useFetch(key, action, {
suspense: true,
errorBoundary: true
});
Component Setup
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<LoadingSpinner />}>
<UserList />
</Suspense>
</ErrorBoundary>
Why
- Errors are propagated to the nearest Error Boundary
- Prevents component errors from crashing the entire app
- Keeps UI predictable and fault-tolerant
Fallback Resolution Hierarchy
Why
- Closest boundary takes precedence to isolate loading/errors
- Avoids unnecessary global fallbacks
- Provides fine-grained control over UX
Route-Level vs Page-Level Boundaries
Route-Level Only
- Lazy-loaded page triggers loading → route fallback shown
Page-Level Only
- Inner component triggers loading → page fallback shown
Nested Example
<Route element={<Suspense fallback={<RouteLoader />}><Page /></Suspense>} />
Page:
<Suspense fallback={<PageLoader />}>
<Inner />
</Suspense>
Innertriggers loading → PageLoader shows- RouteLoader ignored
Why
- Local boundaries reflect the closest relevant state
- Reduces full-page flashes and improves perceived performance
Lazy Loading + Suspense Interactions
Example Lazy Component
const LazyWidget = Loadable({
factory: () => import('./Widget')
});
Combined with Suspense + React Fetch
- Lazy component throws a Promise for module loading
useFetch(key, () => {}, { suspense: true })throws a Promise for data- Each boundary renders its own fallback in order
Timeline
Why
- Allows progressive hydration
- Keeps fallback handling modular
- Ensures errors and loading are isolated
Combined Behaviors and Async Waterfalls
- Multiple async sources (lazy + data) can cause sequential fallbacks
- Can lead to waterfall loading if not planned
- Parallel prefetching can reduce total load time
Waterfall Effect
Parallel Loading
Recommendations
- Group related lazy imports
- Prefetch critical data
- Use skeletons for smoother progressive loading
- Reduce deeply nested Suspense boundaries
Risks & Pitfalls
| Issue | Cause | Mitigation |
|---|---|---|
| Multiple fallback flashing | Nested Suspense + lazy + data | Consolidate boundaries, use skeletons |
| Deeply nested boundaries | Independent async operations | Merge boundaries where possible |
| Missing Suspense | Lazy or fetch throws | Always wrap async code in Suspense |
| Route transitions | Lazy + data fetch | Prefetch modules and/or data |
| Lazy preload missing | Route starts fetch late | Use preload hints |
| Error boundaries mismatch | Lazy-loaded component import fails | Wrap lazy components in proper ErrorBoundary |
| Async waterfall | Sequential lazy → data | Parallel loading, prefetch data |
| Memory leaks | Overusing enduring: true | Only mark critical global data as enduring |
| Stale data | TTL too long | Adjust TTL based on data volatility |
| Excessive fetches | TTL too short or ttl: 0 | Use appropriate TTL with refetch |
Why These Design Patterns Matter
- Ensures consistent, predictable user experience
- Avoids unnecessary crashes
- Enables fine-grained control over loading and error states
- Improves perceived performance and UX
- Simplifies developer mental model for complex async UI
- Optimizes memory usage and network efficiency