React Integration
TanStack Query hooks on every endpoint — direct fetch and hooks on the same object. Zero codegen, full type safety.
Why @cleverbrush/client/react?
The Problem
Using TanStack Query with typed API clients means manually writing queryKey arrays, queryFn wrappers, and mutation boilerplate for every endpoint. Query keys are stringly-typed and fragile — rename an endpoint and your invalidation logic silently breaks.
The Solution
@cleverbrush/client/react wraps your defineApi() contract in a unified client where every endpoint is both a callable function (direct fetch) and an object with TanStack Query hooks. Query keys are hierarchical and deterministic — invalidate a single entry, an entire endpoint, or a whole group with one call. One import, zero boilerplate.
Quick Start
1. Create a Unified Client
Wrap your existing API contract with createClient:
import { createClient } from '@cleverbrush/client/react'; import { api } from 'shared/contract'; export const client = createClient(api, { baseUrl: 'https://api.example.com', });
2. Use It — Direct Fetch or Hooks
// Direct fetch (same as @cleverbrush/client) const todos = await client.todos.list(); const todo = await client.todos.get({ params: { id: 1 } }); // React Query hooks — on the same object function TodoList() { const { data, isLoading } = client.todos.list.useQuery(); if (isLoading) return <p>Loading…</p>; return ( <ul> {data?.map(t => <li key={t.id}>{t.title}</li>)} </ul> ); }
Endpoint API
Every endpoint on the unified client is callable and provides these methods:
| Method | Description |
|---|---|
(args?) | Direct HTTP fetch — returns Promise<Response> |
.stream(args?) | NDJSON streaming — returns AsyncIterable<string> |
useQuery(args?, options?) | Standard TanStack Query hook with auto-generated key and fetch function |
useSuspenseQuery(args?, options?) | Suspense-enabled query — suspends the component until data is ready |
useInfiniteQuery(argsFn, options) | Infinite scrolling with a function that receives pageParam |
useMutation(options?) | Mutation hook — pass endpoint args via mutate(args) |
queryKey(args?) | Returns the query key array for cache operations |
prefetch(queryClient, args?) | Prefetches data into the TanStack query cache |
Hierarchical Query Keys
Query keys follow a predictable hierarchy for fine-grained or bulk invalidation:
// Group key — invalidate ALL endpoints in a group client.todos.queryKey() // → ['@cleverbrush', 'todos'] // Endpoint key — invalidate all variants of one endpoint client.todos.list.queryKey() // → ['@cleverbrush', 'todos', 'list'] // Specific key — target one cache entry client.todos.get.queryKey({ params: { id: 42 } }) // → ['@cleverbrush', 'todos', 'get', { params: { id: 42 } }]
Mutations & Cache Invalidation
import { useQueryClient } from '@tanstack/react-query'; function CreateTodo() { const queryClient = useQueryClient(); const mutation = client.todos.create.useMutation({ onSuccess: () => { // Invalidate all todo queries after creating queryClient.invalidateQueries({ queryKey: client.todos.queryKey() }); }, }); return ( <button onClick={() => mutation.mutate({ body: { title: 'New' } })}> Add Todo </button> ); }
Suspense
import { Suspense } from 'react'; function TodoListSuspense() { const { data } = client.todos.list.useSuspenseQuery(); return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>; } function App() { return ( <Suspense fallback={<p>Loading…</p>}> <TodoListSuspense /> </Suspense> ); }
Prefetching
Pre-populate the cache before a component mounts — great for hover-to-prefetch patterns:
const queryClient = useQueryClient(); <button onMouseEnter={() => client.todos.get.prefetch(queryClient, { params: { id: 1 } }) } > View Todo </button>
Error Handling
Errors are typed as WebError from @cleverbrush/client. Use the provided type guards:
import { isApiError, isTimeoutError } from '@cleverbrush/client'; function TodoList() { const { data, error } = client.todos.list.useQuery(); if (isApiError(error)) { return <p>API error: {error.status} — {error.body?.message}</p>; } if (isTimeoutError(error)) { return <p>Request timed out after {error.timeout}ms</p>; } return <ul>{data?.map(t => <li key={t.id}>{t.title}</li>)}</ul>; }
Key Utilities
For manual key construction outside of the proxy (e.g., in SSR loaders or utility functions):
import { buildQueryKey, buildGroupQueryKey, QUERY_KEY_PREFIX } from '@cleverbrush/client/react'; buildGroupQueryKey('todos'); // → ['@cleverbrush', 'todos'] buildQueryKey('todos', 'list'); // → ['@cleverbrush', 'todos', 'list'] buildQueryKey('todos', 'get', { params: { id: 1 } }); // → ['@cleverbrush', 'todos', 'get', { params: { id: 1 } }]