Query Subscriptions
Problem
You want to react to every state transition of a query — data arriving, loading starting, an error being thrown — from a single subscription point rather than checking state in a render loop.
Solution
Subscriptions give one stable mental model for UI state: subscribe once, render from QueryState, and trigger reads imperatively.
ts
const api = createApi({ baseUrl: 'https://api.example.com' });
const qc = createQuery({ staleTime: 5_000 });
const stop = qc.subscribe<User>(['users', userId], (state) => {
if (state.isFetching) renderSpinner();
if (state.status === 'error') toast.error(state.error!.message);
if (state.status === 'success') renderUser(state.data!);
});
await qc.query({
key: ['users', userId],
fn: ({ signal }) => api.get<User>('/users/{id}', { params: { id: userId }, signal }),
});
stop();
// Retry policy can be set per query call
const retryingQc = createQuery();
await retryingQc.query({
key: ['config'],
fn: ({ signal }) => api.get('/config', { signal }),
attempts: 3,
shouldRetry: (err) => !HttpError.is(err) || (err.status ?? 500) >= 500,
});Pitfalls
- The
onDatacallback fires on every successful response, including background revalidations. Avoid one-time side effects (analytics events, success toasts) inside it without ahasNotifiedguard. - Subscribing in a render cycle without returning an unsubscribe function leaks the listener. Always clean up in
useEffect's return function oronUnmounted. - The callback is not debounced. Rapid successive state changes (e.g., polling + manual refresh) fire the callback for each — guard with a ref if only the latest value matters.