| name | react-query-v3-patterns |
| description | WHAT: React Query v3.39.0 patterns for web data fetching with OpenTelemetry. WHEN: using useQuery v3, useMutation v3, implementing caching on web. KEYWORDS: react-query, v3, useQuery, useMutation, useFetch, cache, web, tracing, RequestIds. |
React Query v3 Patterns - Web
Data fetching patterns using react-query v3.39.0 (NOT TanStack Query v5) with custom useFetch hook and OpenTelemetry tracing.
Documentation
This skill has comprehensive documentation:
When to Use
Use React Query for:
- Fetching, caching, and synchronizing server state
- API calls with automatic caching and revalidation
- Data mutations with optimistic updates
- Infinite scrolling or pagination
- Background data refetching
Note: This codebase uses react-query v3.39.0, not the newer @tanstack/react-query v5. The API is different!
Core Principles
1. useQuery for Data Fetching
Use useQuery hook with query keys and custom useFetch integration.
✅ Good:
import { useQuery } from 'react-query';
import localFetch, { useFetch } from '@/libs/fetch';
import { useLocalizeParams } from '../utils';
import RequestIds from '../RequestIds';
export const useValidateVoucher: UseQuery<
VoucherValidateResult,
VoucherValidateParams
> = (params, options = {}) => {
const { fetch } = useFetch();
const localizedParams = {
...params,
...useLocalizeParams(),
};
const queryKey = [RequestIds['voucher.validate'], localizedParams];
return useQuery<VoucherValidateResult>(
queryKey,
() => {
return validateVoucher(localizedParams, queryKey, fetch);
},
options
);
};
Why: Query keys enable automatic caching and invalidation. The useFetch hook provides OpenTelemetry tracing and error handling.
2. Structured Query Keys
Use arrays with RequestIds and params for consistent query keys.
✅ Good:
import { useQuery } from 'react-query';
import RequestIds from '../RequestIds';
const queryKey = [
RequestIds['reactivation.subscription'],
{
subscriptionId,
locale,
systemCountry,
...otherParams
}
];
return useQuery<SubscriptionResponse>(queryKey, fetchFunction, options);
❌ Bad:
const queryKey = 'subscription';
const queryKey = ['subscription', subscriptionId, locale];
Why: Structured keys with RequestIds ensure uniqueness and make cache invalidation easier. Including params in the key ensures cache correctness.
3. useMutation for Data Changes
Use useMutation for POST, PUT, DELETE operations.
✅ Good:
import { useMutation } from 'react-query';
import { useFetch } from '@/libs/fetch';
export const usePostDistributablesBenefitsMutation: UseMutation<
PostDistributablesBenefitsResponse,
PostDistributablesBenefitsParams
> = (options = {}) => {
const { fetch } = useFetch();
const localizedParams = useLocalizeParams();
const mutationKey = [
RequestIds['voucher-service.distributables.mutate.benefits'],
];
return useMutation(
(params) =>
postDistributablesBenefits(
{ ...params, ...localizedParams },
mutationKey,
fetch
),
options
);
};
Why: Mutations handle side effects like creating/updating data and can trigger cache invalidations.
4. Integration with useFetch + OpenTelemetry
Always use custom useFetch hook for tracing and error handling.
✅ Good:
import localFetch, { useFetch } from '@/libs/fetch';
const postDistributablesBenefits: DataAccessPost<
PostDistributablesBenefitsResponse,
PostDistributablesBenefitsParams
> = async (params, queryKey, fetch = localFetch) => {
const response = await fetch(
`/voucher-service/distributables/${voucherCode}/benefits`,
queryKey,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: {
customer_id: customerId,
plan_id: planId,
},
query: {
country: systemCountry,
locale,
},
parentSpan,
}
);
return response.json();
};
export const useMutation = (options = {}) => {
const { fetch } = useFetch();
return useMutation(
(params) => postDistributablesBenefits(params, mutationKey, fetch),
options
);
};
Why: The useFetch hook adds OpenTelemetry tracing to all API calls, enabling performance monitoring and debugging.
5. TypeScript Generic Types
Use TypeScript generics for type-safe queries and mutations.
✅ Good:
import { UseQueryOptions } from 'react-query';
export const getReactivateSubscriptions: DataAccessGet<
SubscriptionResponse,
GetReactivationSubscriptionsParams
> = async (params, queryKey, fetch = localFetch) => {
const result = await fetch('/reactivation-subscription', queryKey, {
method: 'GET',
query: {
country: params.systemCountry,
locale: params.locale,
},
});
return result.json();
};
Why: Type safety ensures params and responses are correct at compile time.
Query Options
Common Options
useQuery(queryKey, fetchFn, {
retry: 3,
});
useQuery(queryKey, fetchFn, {
staleTime: 5 * 60 * 1000,
});
useQuery(queryKey, fetchFn, {
cacheTime: 10 * 60 * 1000,
});
useQuery(queryKey, fetchFn, {
enabled: !!subscriptionId,
});
useQuery(queryKey, fetchFn, {
onSuccess: (data) => {
console.log('Data fetched successfully', data);
},
onError: (error) => {
console.error('Fetch failed', error);
},
});
Data Access Pattern
Standard Structure
type Params = { ... };
type Response = { ... };
const fetchData: DataAccessGet<Response, Params> = async (
params,
queryKey,
fetch = localFetch
) => {
const result = await fetch('/endpoint', queryKey, {
method: 'GET',
query: params,
parentSpan,
});
return result.json();
};
export const useDataHook: UseQuery<Response, Params> = (
params,
options = {}
) => {
const { fetch } = useFetch();
const localizedParams = { ...params, ...useLocalizeParams() };
const queryKey = [RequestIds['endpoint.name'], localizedParams];
return useQuery<Response>(
queryKey,
() => fetchData(localizedParams, queryKey, fetch),
options
);
};
File Organization
data-access/
├── RequestIds.ts # Centralized query key constants
├── schema.ts # Shared types
├── utils.ts # useLocalizeParams, etc.
├── reactivation/
│ ├── subscription.ts # Query hooks
│ ├── subscription.test.ts # Tests
│ └── schema.ts # Types
└── voucher/
├── validate.ts # Query hooks
├── validate.spec.ts # Tests
└── schema.ts # Types
Common Mistakes
- Using TanStack Query v5 syntax - This codebase uses react-query v3, not v5!
- Forgetting useFetch integration - Always use useFetch for OpenTelemetry tracing
- String-only query keys - Use arrays with RequestIds and params
- Not including params in query key - Cache won't update when params change
- Importing from wrong package - Use 'react-query' NOT '@tanstack/react-query'
Quick Reference
v3 API (Current)
import { useQuery, useMutation } from 'react-query';
const { data, isLoading, error } = useQuery(queryKey, fetchFn, options);
const { mutate, isLoading } = useMutation(mutateFn, {
onSuccess: (data) => { },
onError: (error) => { },
});
mutate({ param: 'value' });
With useFetch Integration
import { useFetch } from '@/libs/fetch';
const useSomeData = (params, options = {}) => {
const { fetch } = useFetch();
const queryKey = [RequestIds['some.data'], params];
return useQuery(
queryKey,
() => fetchData(params, queryKey, fetch),
options
);
};
Testing
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
render(
<QueryClientProvider client={queryClient}>
<Component />
</QueryClientProvider>
);