MainSDK
Search⌘K
How-to

Call your API from React

Generate React hooks next to the typed client, then run an action with field errors, read and list with a cache, show the session and refresh on live events.

Planned for 0.7

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You have a typed API client. This guide adds hooks so a component can run an action, read a record and page a list without writing fetch, loading state or error mapping. It takes about 15 minutes. The generated file imports only react and your client.

You need

1. Generate the hooks

bash
npx mainsdk api client --out src/acme-api.ts --react

You should see src/acme-api-react.ts next to the client. Use --react-out <file> to put it elsewhere. Run the command again after you change an action; the file is generated, so do not edit it.

2. Give the tree one client

Make the client once, so every component shares it.

acme.ts
import { createClient } from "./acme-api.ts"

export const acme = createClient({ baseUrl: "https://acme.app" })
tsx
import { ApiProvider } from "./acme-api-react.ts"
import { acme } from "./acme.ts"

export function Providers({ children }: { children: React.ReactNode }) {
  return <ApiProvider client={acme}>{children}</ApiProvider>
}

3. Run an action and show field errors

tsx
import { useAction } from "./acme-api-react.ts"
import { acme } from "./acme.ts"

export function RefundForm({ orderId, reason }: { orderId: string; reason: "damaged" | "late" }) {
  const refund = useAction(acme.orders.refund, { invalidates: ["orders.*"] })
  return (
    <form onSubmit={(e) => { e.preventDefault(); void refund.run({ orderId, reason }) }}>
      <input name="reason" />
      {refund.fieldErrors.reason && <p>{refund.fieldErrors.reason}</p>}
      <button disabled={refund.pending}>Refund</button>
    </form>
  )
}

invalidates names the reads to refetch when the action succeeds. A wrong input is a compile error, because the types come from your client.

4. Read and list

tsx
import { useList, useRead } from "./acme-api-react.ts"
import { acme } from "./acme.ts"

export function Orders({ orderId }: { orderId: string }) {
  const order = useRead(acme.orders.get, { orderId }, { staleTime: "30s" })
  const orders = useList(acme.orders.list, { status: "open" }, { limit: 20 })
  return <p>{order.data?.status} {orders.items.length}</p>
}

orders.items holds every page loaded so far and orders.more() loads the next. Two components that read the same input share one request.

5. Session and live updates

tsx
import { useLive, useSession } from "./acme-api-react.ts"

export function Header() {
  const { session, pending } = useSession()
  useLive(["order:42"], { invalidates: ["orders.get"] })
  return <p>{pending ? "…" : session ? "Signed in" : "Signed out"}</p>
}

useLive refetches the reads it names when an event arrives, and closes its subscription when the last component using it unmounts.

6. Use TanStack Query instead (optional)

If the app already uses TanStack Query, take the options and keep its cache.

tsx
import { useMutation, useQuery } from "@tanstack/react-query"
import { mutationOptions, queryOptions } from "./acme-api-react.ts"
import { acme } from "./acme.ts"

export function Order({ orderId }: { orderId: string }) {
  const q = useQuery(queryOptions(acme.orders.get, { orderId }))
  const m = useMutation(mutationOptions(acme.orders.refund))
  return <button onClick={() => m.mutate({ orderId, reason: "late" })}>{q.data?.status}</button>
}

Check it worked

Run the app, open a page that uses useRead twice with the same input, and look in the network tab: one request, not two. Submit the form with a bad reason and the field error shows without a page reload.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet