packages/cli/skills/pikku-react-query/SKILL.md
Use the Pikku auto-generated React Query hooks (`usePikkuQuery`, `usePikkuMutation`, `usePikkuInfiniteQuery`) to call backend RPC functions from a React frontend with full type safety. TRIGGER when: writing React components that need to call a Pikku function, fetch data, mutate data, or paginate; user mentions React Query, useQuery, useMutation, or building a frontend that talks to a Pikku backend. DO NOT TRIGGER when: working on the backend (use pikku-rpc / pikku-feature) or wiring a non-React frontend.
npx skillsauth add pikkujs/pikku pikku-react-queryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill as an execution checklist, not reference material.
pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.Pikku generates a typed React Query layer from your backend expose: true
functions. You don''t write useQuery/useMutation against fetch
yourself — you call hooks named after RPCs and get full type inference for
input + output.
Before writing a hook, get the full client surface in one call:
yarn pikku meta clients --json
Returns RPCs, workflows, and channels with descriptions and type names:
{
"rpcs": [
{ "name": "createTodo", "description": "Create a todo",
"readonly": false, "input": "CreateTodoInput", "output": "CreateTodoOutput" },
{ "name": "listTodos", "description": "List all todos",
"readonly": true, "input": null, "output": "ListTodosOutput" }
],
"workflows": [...],
"channels": [...]
}
The name is the RPC identifier; pass it to the hooks below. Input/output
shapes are inferred automatically — the hook is typed against
FlattenedRPCMap[name]['input' | 'output']. Use description to pick the
right RPC; use readonly to choose usePikkuQuery vs usePikkuMutation.
In your app entry (e.g. main.tsx):
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: import.meta.env.VITE_API_URL ?? 'http://localhost:3000',
})
<QueryClientProvider client={queryClient}>
<PikkuProvider pikku={pikku}>
<App />
</PikkuProvider>
</QueryClientProvider>
The two generated files come from pikku.config.json's
clientFiles.fetchFile and clientFiles.rpcWiringsFile. Hooks live in
the file at clientFiles.reactQueryFile (typically api.gen.ts).
All hooks are imported from your generated api.gen.ts:
import {
usePikkuQuery,
usePikkuMutation,
usePikkuInfiniteQuery,
} from './pikku/api.gen'
usePikkuQuery(name, data, options?)For RPCs that read data. Cacheable. The hook is typed against the RPC's input + output.
export function TodoList() {
const { data, isLoading, error } = usePikkuQuery('listTodos', {})
if (isLoading) return <p>Loading…</p>
if (error) return <p>{error.message}</p>
return (
<ul>
{data?.todos.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
)
}
The query key is [name, data] automatically — no manual key wrangling.
Pass standard useQuery options through (staleTime, enabled, etc.).
usePikkuMutation(name, options?)For RPCs that write. Returns a React Query mutation object.
export function CreateTodoForm() {
const queryClient = useQueryClient()
const mutation = usePikkuMutation('createTodo', {
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['listTodos'] }),
})
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const title = (
e.currentTarget.elements.namedItem('title') as HTMLInputElement
).value
mutation.mutate({ title })
}
return (
<form onSubmit={onSubmit}>
<input name="title" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Adding…' : 'Add'}
</button>
</form>
)
}
The input passed to mutation.mutate(...) is type-checked against the RPC's
input schema. After success, invalidate any list/get queries that should
refetch.
usePikkuInfiniteQuery(name, data, options?)Only available for RPCs whose output has a nextCursor?: string | null
field — typically a list endpoint with pagination. The hook auto-feeds
nextCursor into the next page's request.
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
usePikkuInfiniteQuery('listTodos', { limit: 20 })
const todos = data?.pages.flatMap((p) => p.rows) ?? []
If the hook isn't generated for an RPC, the RPC's output doesn't include
nextCursor — paginate it on the backend or use usePikkuQuery with
manual cursor state.
When the project has workflows (capabilities.workflow: true), three
extra hooks are generated. See the pikku-workflows-client skill.
For one-off calls (event handlers outside of state, side effects), use
usePikkuRPC() from @pikku/react:
const rpc = usePikkuRPC()
const handleClick = async () => {
const result = await rpc.invoke('createTodo', { title: 'inline' })
}
But prefer the React Query hooks for anything that touches render state — caching, retries, dedup, and dev-tools come for free.
onMutate to usePikkuMutation to update
the cache before the server responds. Standard React Query pattern;
Pikku doesn't add anything special.enabled: !!someValue to skip a query
until you have the input.refetchOnWindowFocus: false in options.useEffect —
use the hooks. They handle dedup, caching, and unmount safely.useQuery({ queryKey: ['listTodos'], queryFn: ... })
— usePikkuQuery('listTodos', {}) does it correctly with one line.as any — if a hook's types don't
match what you expect, the backend's input/output schemas are wrong;
fix those first.documentation
Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.
testing
Use when adding authorization checks to Pikku functions or routes — pikkuPermission, pikkuAuth, per-function permissions, pattern-based permissions, or understanding OR/AND permission logic. TRIGGER when: user wants to restrict who can call a function, check resource ownership, add role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks about middleware or request interception (use pikku-middleware), authentication strategies (use pikku-security), or session management.
testing
Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority. TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment.
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.