Fast lookup for common TanStack Query patterns in this project.
// client/src/queries/myFeature.ts
import { queryOptions } from '@tanstack/react-query';
export interface MyData {
id: string;
name: string;
}
async function fetchMyData(id: string): Promise<MyData> {
const response = await fetch(`/api/my-feature/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}
export function myDataQueryOptions(id: string) {
return queryOptions({
queryKey: ['myFeature', 'data', { id }],
queryFn: () => fetchMyData(id),
staleTime: 1000 * 60, // 1 minute
});
}// client/src/queries/index.ts
export { myDataQueryOptions, type MyData } from './myFeature';import { useQuery } from '@tanstack/react-query';
import { myDataQueryOptions } from '../queries';
function MyComponent({ id }: { id: string }) {
const { data, isLoading, error } = useQuery({
...myDataQueryOptions(id),
});
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return <div>{data.name}</div>;
}// client/src/queries/myFeature.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
async function createMyData(input: CreateInput): Promise<void> {
const response = await fetch('/api/my-feature', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error('Failed to create');
}
export function useCreateMyDataMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createMyData,
onSuccess: () => {
// Invalidate to refresh list
queryClient.invalidateQueries({ queryKey: ['myFeature', 'data'] });
},
});
}import { useToast } from '../lib/toast';
function MyForm() {
const mutation = useCreateMyDataMutation();
const { showToast } = useToast();
const handleSubmit = (data: CreateInput) => {
mutation.mutate(data, {
onSuccess: () => showToast('Created!', 'success'),
onError: (err) => showToast(err.message, 'error'),
});
};
return (
<button onClick={handleSubmit} disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create'}
</button>
);
}// client/src/hooks/usePrefetchQueries.ts
export function usePrefetchQueries() {
const queryClient = useQueryClient();
const prefetchMyData = useCallback((id: string) => {
queryClient.prefetchQuery(myDataQueryOptions(id));
}, [queryClient]);
return {
// ... other prefetch functions
prefetchMyData,
};
}function MyComponent() {
const { prefetchMyData } = usePrefetchQueries();
return (
<button
onClick={() => navigate('/my-page')}
onMouseEnter={() => prefetchMyData('123')}
>
Go to Page
</button>
);
}import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: Infinity },
},
});
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}const fetchMock = vi.fn();
beforeEach(() => {
fetchMock.mockReset();
fetchMock.mockResolvedValue({
ok: true,
json: async () => ({ /* data */ }),
});
vi.stubGlobal('fetch', fetchMock);
});// ✅ Use findByText (waits automatically)
const element = await screen.findByText('Loaded');
// ❌ Don't use waitFor + getByText
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});| Entity | Query Key Pattern | Example |
|---|---|---|
| Games | ['games', 'recent', { page, limit, filter }] |
['games', 'recent', { page: 1, limit: 10, filter: 'all' }] |
| Leaderboard | ['leaderboard', { limit }] |
['leaderboard', { limit: 50 }] |
| Stats | ['stats', 'home'] or ['stats', 'about'] |
['stats', 'home'] |
| Analysis | ['analysis', 'game', gameId] |
['analysis', 'game', 'abc123'] |
| Fair Play | ['fairPlay', 'cases', { page, limit, status }] |
['fairPlay', 'cases', { page: 1, limit: 20, status: 'all' }] |
| Feedback | ['feedback', 'messages', { page, limit, type }] |
['feedback', 'messages', { page: 1, limit: 20, type: 'all' }] |
| Data Type | Stale Time | Reason |
|---|---|---|
| Games list | 30-60s | Changes frequently |
| Leaderboard | 2 minutes | Changes less often |
| Stats | 30s | Real-time-ish |
| User profile | 5 minutes | Rarely changes |
| Static content | 10+ minutes | Almost never changes |
const { data, isLoading, error, refetch } = useQuery({
...myQueryOptions(),
});
if (isLoading) return <Loading />;
if (error) {
return (
<ErrorState
message={error.message}
onRetry={refetch} // Allow retry
/>
);
}Already enabled in client/src/main.tsx:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>Press Shift + Alt + T to open DevTools in browser.
const queryClient = new QueryClient({
logger: {
log: console.log,
warn: console.warn,
error: console.error,
},
});- Create query options factory in
client/src/queries/ - Export from
client/src/queries/index.ts - Replace
useEffect+useStatewithuseQuery - Add loading state handling
- Add error state handling
- Add mutation with invalidation
- Add toast notifications
- Add prefetching if needed
- Update tests with QueryClientProvider
- Add fetch mock with
ok: true - Use
findByTextfor async tests
- Check
docs/tanstack-query-patterns.mdfor detailed guide - Look at existing implementations in
client/src/queries/ - See component examples in
client/src/components/ - Review tests in
client/src/test/