From harness-claude
Provides typed React hooks (useQuery, useMutation) for tRPC procedures with TanStack Query cache invalidation and optimistic updates.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:trpc-react-query-integrationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> End-to-end type-safe data fetching with `api.xxx.useQuery`, `useMutation`, and cache invalidation via TanStack Query
End-to-end type-safe data fetching with
api.xxx.useQuery,useMutation, and cache invalidation via TanStack Query
// lib/api.ts (or lib/trpc/client.tsx)
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/root';
export const api = createTRPCReact<AppRouter>();
All hooks (useQuery, useMutation, useSubscription) are available on api.<router>.<procedure>.
'use client';
import { api } from '@/lib/api';
function PostList() {
const { data, isLoading, error } = api.post.list.useQuery(
{ limit: 20 },
{
staleTime: 60_000, // Fresh for 60s — won't refetch on mount
refetchOnWindowFocus: false, // Disable auto-refetch on tab focus
}
);
if (isLoading) return <Skeleton />;
if (error) return <Error message={error.message} />;
return <ul>{data.map(post => <li key={post.id}>{post.title}</li>)}</ul>;
}
function CreatePostForm() {
const utils = api.useUtils();
const createPost = api.post.create.useMutation({
onSuccess: (newPost) => {
// Invalidate and refetch the post list
void utils.post.list.invalidate();
},
onError: (error) => {
toast.error(error.message);
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
createPost.mutate({
title: data.get('title') as string,
content: data.get('content') as string,
});
}}>
<input name="title" />
<button type="submit" disabled={createPost.isPending}>
{createPost.isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
const utils = api.useUtils();
const likePost = api.post.like.useMutation({
onMutate: async ({ postId }) => {
// Cancel outgoing queries for this data
await utils.post.getById.cancel({ id: postId });
// Snapshot the current value
const previous = utils.post.getById.getData({ id: postId });
// Optimistically update the cache
utils.post.getById.setData({ id: postId }, (old) =>
old ? { ...old, likeCount: old.likeCount + 1 } : old
);
return { previous };
},
onError: (err, { postId }, context) => {
// Roll back on failure
if (context?.previous) {
utils.post.getById.setData({ id: postId }, context.previous);
}
},
onSettled: ({ postId }) => {
// Always refetch to sync server truth
void utils.post.getById.invalidate({ id: postId });
},
});
// Prefetch on hover for instant page transitions
function PostLink({ postId }: { postId: string }) {
const utils = api.useUtils();
return (
<a
href={`/posts/${postId}`}
onMouseEnter={() => {
void utils.post.getById.prefetch({ id: postId });
}}
>
View Post
</a>
);
}
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
api.post.listInfinite.useInfiniteQuery(
{ limit: 20 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialCursor: undefined,
}
);
const allPosts = data?.pages.flatMap((page) => page.items) ?? [];
End-to-end type inference. The AppRouter type propagates through createTRPCReact<AppRouter>() to every hook. useQuery's data type is inferred from the procedure's return type. useMutation's variables type is inferred from .input(). No manual type annotations required anywhere on the client.
api.useUtils() is the query client proxy. It provides typed access to TanStack Query cache operations scoped to tRPC procedures: utils.post.list.invalidate(), utils.post.list.setData(), utils.post.list.prefetch(). These are type-safe wrappers over queryClient.invalidateQueries, setQueryData, etc.
Query key structure. tRPC generates stable query keys from the procedure path and input. api.post.getById.useQuery({ id: '1' }) and api.post.getById.useQuery({ id: '2' }) have distinct cache entries. utils.post.getById.invalidate() (no argument) invalidates all entries for getById. utils.post.getById.invalidate({ id: '1' }) invalidates only the specific entry.
useMutation vs mutate vs mutateAsync. mutate() is fire-and-forget — errors are handled via onError. mutateAsync() returns a Promise — you can await it and handle errors with try/catch. Use mutateAsync in form submit handlers where you need to control flow after the mutation.
Error types. error from useQuery/useMutation is a TRPCClientError<AppRouter>. Access error.data?.code for the tRPC error code, error.data?.zodError for field-level validation errors (if your server formats them), and error.message for the human-readable message.
Suspense mode. Replace useQuery with useSuspenseQuery to use React Suspense for loading states. The component suspends while loading and renders only when data is available — data is always defined (never undefined).
https://trpc.io/docs/client/react
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeSets up tRPC with Next.js App Router using the fetch adapter, server-side callers for RSC, and client components via React Query.
Integrates backend APIs with frontend apps using type-safe patterns: tRPC for E2E type safety, OpenAPI codegen (openapi-typescript, orval), TanStack Query/SWR for data fetching, optimistic updates, error handling.
Builds end-to-end type-safe tRPC APIs with routers, procedures, middleware, subscriptions, and Next.js/React integration for TypeScript full-stack apps.