Apollo 12: Suspense Hooks
easy⏱ 5 mincourseapollo
Suspense-compatible hooks
Apollo provides three Suspense hooks:
- useSuspenseQuery: Suspends the component while the query loads. No
loading boolean — the Suspense boundary handles it. Returns { data, error }.
- useBackgroundQuery: Starts a query in a parent component and returns a
queryRef. The child consumes it with useReadQuery(queryRef), which suspends. This avoids request waterfalls because the parent starts fetching before the child mounts.
- useLoadableQuery: Returns
[loadQuery, queryRef] — the query only executes when you call loadQuery(). Perfect for user-triggered data loading (click to load detail).
import { Suspense } from 'react';
import { useSuspenseQuery } from '@apollo/client';
function UserProfile({ id }) {
// This suspends — no loading state needed!
const { data } = useSuspenseQuery(GET_USER, {
variables: { id },
});
return <h2>{data.user.name}</h2>;
}
function App() {
return (
<Suspense fallback={<p>Loading profile...</p>}>
<UserProfile id="1" />
</Suspense>
);
}
Convert useQuery to Suspense
Replace useQuery(GET_USERS) with useSuspenseQuery(GET_USERS) in a UserList component. Remove the if (loading) check — Suspense handles it now. Wrap the component in <Suspense fallback={<Skeleton />}>. Then use useBackgroundQuery in a parent to start fetching user details while the list is still rendering.
startTransition for smooth UX
Wrap variable changes in startTransition to keep showing the current UI while new data loads: startTransition(() => setUserId('2')). Without it, React Suspense would immediately show the fallback. With startTransition, the old data stays visible (in a "pending" state) until the new data is ready. Combine with useTransition for an isPending boolean to show a subtle loading indicator.
import { useTransition } from 'react';
function UserList() {
const [isPending, startTransition] = useTransition();
const [userId, setUserId] = useState('1');
const selectUser = (id) => {
startTransition(() => setUserId(id));
};
return (
<div style={{ opacity: isPending ? 0.7 : 1 }}>
{/* content */}
</div>
);
}
Avoiding waterfalls with useBackgroundQuery
A request waterfall happens when a parent fetches data, then a child mounts and starts its own fetch. The child's request is delayed by the parent's loading time. useBackgroundQuery solves this: the parent starts both requests simultaneously. The parent renders <Suspense> around the child, which uses useReadQuery(queryRef) to consume the already-in-flight query. Both queries load in parallel instead of sequentially.
function Parent() {
const [queryRef] = useBackgroundQuery(GET_DETAILS, {
variables: { id: '1' },
});
return (
<Suspense fallback={<p>Loading details...</p>}>
<Child queryRef={queryRef} />
</Suspense>
);
}
function Child({ queryRef }) {
const { data } = useReadQuery(queryRef);
return <p>{data.details.title}</p>;
}