Skip to main content

Overview

Contract Kit provides optional React integrations that work seamlessly with your contracts. These integrations are completely optional - you can use Contract Kit without React or choose which React packages fit your needs.

Available Integrations

Why React Integration?

While you can use the Contract Kit client directly in React, the integration packages provide:
  1. Contract-aware Query Helpers - Generate typed query/mutation options from contracts
  2. Caching & Revalidation - Built-in data management with React Query
  3. Form Validation - Automatic form validation with React Hook Form
  4. Optimistic Updates - Easy optimistic UI updates
  5. Loading States - Automatic loading and error state management

Quick Comparison

Without React Integration

import { useState, useEffect } from "react";
import { client } from "@/lib/api-client";
import { listTodos } from "@/contracts/todo";

const listTodosEndpoint = client.endpoint(listTodos);

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    listTodosEndpoint.call({ query: {} })
      .then(result => setTodos(result.todos))
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

With React Query Integration

import { useQuery } from "@tanstack/react-query";
import { rq } from "@/lib/rq";
import { listTodos } from "@/contracts/todo";

function TodoList() {
  const todoQuery = rq(listTodos).queryOptions({ query: {} });
  const { data, isLoading, error } = useQuery(todoQuery);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

Installation

React Query

npm install @contract-kit/react-query @tanstack/react-query

React Hook Form

npm install @contract-kit/react-hook-form react-hook-form @hookform/resolvers

Base React Utilities

npm install @contract-kit/react

Setup

React Query Setup

// app/providers.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 minute
      retry: 1,
    },
  },
});

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Create a React Query Adapter

// lib/rq.ts
import { createRQ } from "@contract-kit/react-query";
import { apiClient } from "./api-client";

export const rq = createRQ(apiClient);

Features Overview

React Query Integration

  • ✅ Options-first query/mutation helpers
  • ✅ Intelligent caching and revalidation
  • ✅ Optimistic updates
  • ✅ Parallel and dependent queries
  • ✅ Infinite queries
  • ✅ Query invalidation
  • ✅ DevTools integration
Learn More →

React Hook Form Integration

  • ✅ Automatic form validation
  • ✅ Type-safe form fields
  • ✅ Error handling
  • ✅ Form state management
  • ✅ Submission handling
  • ✅ Field-level validation
Learn More →

Usage Examples

Data Fetching

import { useQuery } from "@tanstack/react-query";
import { rq } from "@/lib/rq";
import { getTodo } from "@/contracts/todo";

function TodoDetail({ id }: { id: string }) {
  const todoQuery = rq(getTodo).queryOptions({
    path: { id },
  });
  const { data, isLoading, error, refetch } = useQuery(todoQuery);

  if (isLoading) return <Spinner />;
  if (error) return <Error error={error} />;

  return (
    <div>
      <h1>{data.title}</h1>
      <p>Status: {data.completed ? "Done" : "Pending"}</p>
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  );
}

Mutations

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { rq } from "@/lib/rq";
import { createTodo } from "@/contracts/todo";

function CreateTodoForm() {
  const queryClient = useQueryClient();
  const createMutation = useMutation(
    rq(createTodo).mutationOptions({
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ["todos"] });
      },
    })
  );

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await createMutation.mutateAsync({
      body: {
        title: "New Todo",
        completed: false,
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="title" />
      <button disabled={createMutation.isPending}>
        {createMutation.isPending ? "Creating..." : "Create"}
      </button>
    </form>
  );
}

Forms with Validation

import { rhf } from "@contract-kit/react-hook-form";
import { client } from "@/lib/api-client";

const createTodoEndpoint = client.endpoint(createTodoContract);

function TodoForm() {
  const { useForm } = rhf(createTodoContract);
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = async (data) => {
    // data is fully typed and validated
    await createTodoEndpoint.call({ body: data });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("title")} />
      {errors.title && <span>{errors.title.message}</span>}

      <button type="submit">Create</button>
    </form>
  );
}

Best Practices

Group query helpers by resource (todos, users, posts) for better organization.
Set sensible defaults in your QueryClient for staleTime, retry, etc.
Always provide UI feedback for loading and error states.
Ensure your UI stays in sync by invalidating related queries after mutations.
Implement optimistic updates for instant feedback on user actions.

Next Steps