React Query Hooks
Pikku generates fully typed TanStack React Query hooks from your function definitions. You get useQuery, useMutation, and useInfiniteQuery β with input/output types inferred from your functions. No manual type maintenance.
Setupβ
1. Enable generationβ
Add reactQueryFile to your pikku.config.json:
{
"clientFiles": {
"reactQueryFile": ".pikku/pikku-react-query.gen.ts"
}
}
2. Generate hooksβ
npx pikku all
# or just the hooks:
npx pikku react-query
This generates a file with typed hooks that import from your generated RPC map.
3. Install dependenciesβ
The generated hooks need @tanstack/react-query and @pikku/react:
npm install @tanstack/react-query @pikku/react
4. Wire up the providerβ
Wrap your app with both the React Query and Pikku providers:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { PikkuProvider, createPikku } from '@pikku/react'
import { PikkuFetch } from '.pikku/pikku-fetch.gen'
import { PikkuRPC } from '.pikku/pikku-rpc.gen'
const queryClient = new QueryClient()
const pikku = createPikku(PikkuFetch, PikkuRPC, {
serverUrl: 'https://api.example.com'
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<PikkuProvider pikku={pikku}>
{/* your app */}
</PikkuProvider>
</QueryClientProvider>
)
}
Hooksβ
usePikkuQueryβ
For reading data. Wraps useQuery with type-safe RPC name and input:
import { usePikkuQuery } from '.pikku/pikku-react-query.gen'
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = usePikkuQuery('getUser', { userId })
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
// data is fully typed β matches your function's output type
return <h1>{data.name}</h1>
}
The query key is automatically set to [name, data], so queries with different inputs cache separately.
You can pass any standard useQuery options as the third argument:
usePikkuQuery('getUser', { userId }, { staleTime: 5000, refetchInterval: 10000 })
usePikkuMutationβ
For writes. Wraps useMutation:
import { usePikkuMutation } from '.pikku/pikku-react-query.gen'
function CreateTodo() {
const { mutate, isPending } = usePikkuMutation('createTodo')
return (
<button
disabled={isPending}
onClick={() => mutate({ title: 'Buy milk' })}
>
Add Todo
</button>
)
}
The mutate callback is typed β it only accepts your function's input type and the result matches the output type.
usePikkuInfiniteQueryβ
For paginated data. Only available for functions whose output includes nextCursor:
// This function qualifies for infinite query because its output has nextCursor
export const listItems = pikkuSessionlessFunc<
{ limit: number; nextCursor?: string },
{ items: string[]; nextCursor?: string }
>({
func: async (_services, data) => ({ items: [...], nextCursor: '...' }),
expose: true,
})
import { usePikkuInfiniteQuery } from '.pikku/pikku-react-query.gen'
function ItemList() {
const { data, fetchNextPage, hasNextPage } = usePikkuInfiniteQuery(
'listItems',
{ limit: 20 }
)
return (
<>
{data?.pages.flatMap(page => page.items).map(item => (
<div key={item}>{item}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>Load more</button>
)}
</>
)
}
The cursor handling is wired up automatically β getNextPageParam and initialPageParam are set for you. You just pass the input data without nextCursor (it's managed by the infinite query).
If a function's output doesn't have nextCursor, TypeScript won't let you use it with usePikkuInfiniteQuery. That's intentional.
Workflow Hooksβ
If your project uses Workflows, the generated file also includes:
useRunWorkflow(name)β mutation that runs a workflow to completionuseStartWorkflow(name)β mutation that starts a workflow and returns{ runId }useWorkflowStatus(name, runId)β query that polls workflow status byrunId
Type Safetyβ
The generated hooks are type-checked at compile time. Invalid RPC names, wrong input shapes, and incorrect usage all produce TypeScript errors:
// β
Compiles β correct name and input
usePikkuQuery('getUser', { userId: '123' })
// β TypeScript error β unknown RPC name
usePikkuQuery('doesNotExist', {})
// β TypeScript error β wrong input type
usePikkuQuery('getUser', { wrong: 'field' })
// β TypeScript error β output has no nextCursor
usePikkuInfiniteQuery('getUser', { userId: '123' })
Next Stepsβ
- Exposed RPCs β How functions get exposed to clients
- Fetch Client β Lower-level type-safe HTTP client
- Workflows β Multi-step processes with React Query hooks