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.
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.
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
- The typed client from Publish your actions as a typed API, for example
src/acme-api.ts. react18 or 19 inpackage.json. Without it the command exits 1 and writes nothing.
1. Generate the hooks
npx mainsdk api client --out src/acme-api.ts --reactYou 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.
import { createClient } from "./acme-api.ts"
export const acme = createClient({ baseUrl: "https://acme.app" })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
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
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
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.
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?