React
Contract Kit provides optional integrations for React Query and React Hook Form. Both are fully typed from your contracts.
bun add @contract-kit/react-query @tanstack/react-query
bun add @contract-kit/react-hook-form react-hook-form @hookform/resolvers
Or install both at once with the umbrella package:
bun add @contract-kit/react
React Query
@contract-kit/react-query gives you typed query and mutation options derived from your contracts.
Setup
import { createClient } from "contract-kit";
import { createRQ } from "@contract-kit/react-query";
const client = createClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
});
export const rq = createRQ(client);
Queries
Use rq(contract).queryOptions() with useQuery.
import { useQuery } from "@tanstack/react-query";
import { rq } from "@/app/lib/rq";
import { getTodo } from "@/app/contracts/todo";
function TodoDetail({ id }: { id: string }) {
const { data, isLoading, error } = useQuery(
rq(getTodo).queryOptions({ path: { id } })
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <p>{data.title}</p>; // fully typed
}
Mutations
Use rq(contract).mutationOptions() with useMutation.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { rq } from "@/app/lib/rq";
import { createTodo, listTodos } from "@/app/contracts/todo";
function CreateTodoButton() {
const queryClient = useQueryClient();
const mutation = useMutation(
rq(createTodo).mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: rq(listTodos).key() });
},
})
);
return (
<button onClick={() => mutation.mutate({ body: { title: "New todo" } })}>
Add Todo
</button>
);
}
Query keys
rq(contract).key() generates stable, contract-aware query keys for cache operations.
// Invalidate all getTodo queries
queryClient.invalidateQueries({ queryKey: rq(getTodo).key() });
// Invalidate a specific getTodo query
queryClient.invalidateQueries({ queryKey: rq(getTodo).key({ path: { id: "123" } }) });
Infinite queries
For paginated data, use infiniteQueryOptions.
import { useInfiniteQuery } from "@tanstack/react-query";
const { data, fetchNextPage } = useInfiniteQuery(
rq(listTodos).infiniteQueryOptions({
params: ({ pageParam = 0 }) => ({
query: { offset: pageParam, limit: 10 },
}),
getNextPageParam: (lastPage) =>
lastPage.todos.length < 10 ? undefined : lastPage.total,
})
);
Prefetching
Prefetch data on the server or on user interaction.
// Server-side prefetching
const queryClient = new QueryClient();
await queryClient.prefetchQuery(
rq(getTodo).queryOptions({ path: { id: "123" } })
);
// Prefetch on hover
<button onMouseEnter={() =>
queryClient.prefetchQuery(
rq(getTodo).queryOptions({ path: { id: "123" } })
)
}>
View Todo
</button>
React Hook Form
@contract-kit/react-hook-form gives you form validation derived from your contract's body schema.
Setup
import { rhf } from "@contract-kit/react-hook-form";
import { createTodo } from "@/app/contracts/todo";
const createTodoForm = rhf(createTodo);
Basic form
function CreateTodoForm() {
const form = createTodoForm.useForm({
defaultValues: { title: "", completed: false },
});
const onSubmit = form.handleSubmit(async (data) => {
await createTodoEndpoint.call({ body: data });
});
return (
<form onSubmit={onSubmit}>
<input {...form.register("title")} /> {/* type-safe field names */}
{form.formState.errors.title && (
<span>{form.formState.errors.title.message}</span>
)}
<button type="submit" disabled={form.formState.isSubmitting}>
Create
</button>
</form>
);
}
Validation runs automatically using your contract's body schema. Field names, types, and error messages are all inferred.
With React Query
Combine form validation with mutations for a complete flow.
function CreateTodoForm() {
const form = createTodoForm.useForm({
defaultValues: { title: "", completed: false },
});
const mutation = useMutation(
rq(createTodo).mutationOptions({
onSuccess: () => form.reset(),
onError: (error) => {
form.setError("root", { message: error.message });
},
})
);
const onSubmit = form.handleSubmit((data) => {
mutation.mutate({ body: data });
});
return (
<form onSubmit={onSubmit}>
<input {...form.register("title")} />
{form.formState.errors.title && (
<span>{form.formState.errors.title.message}</span>
)}
{form.formState.errors.root && (
<span>{form.formState.errors.root.message}</span>
)}
<button type="submit" disabled={mutation.isPending}>
Create
</button>
</form>
);
}
Form options
You can also get raw form options if you want to call useForm yourself.
import { useForm } from "react-hook-form";
const form = useForm(createTodoForm.formOptions({
defaultValues: { title: "" },
mode: "onBlur",
}));
Disabling automatic validation
If you want to handle validation manually, set resolverEnabled to false.
const form = createTodoForm.useForm({
resolverEnabled: false,
});