React Server Actions
Call Katman procedures as React Server Actions — type-safe [error, data] tuples instead of try/catch.
React Server Actions let you call server-side code directly from React components. Katman wraps your procedures as actions that return [error, data] tuples instead of throwing, making error handling straightforward in your UI code.
This works with Next.js (App Router) and TanStack Start.
Prerequisites
You need a Katman router defined on the server. If you haven't set one up yet, see the Getting Started guide.
createAction
Wraps a single procedure as a server action. Returns [error, data] instead of throwing.
Define the action in a file with "use server":
// app/actions.ts
"use server"
import { } from "katman/react"
import { } from "../router"
export const = (.users.list)
export const = (.users.create)Use it in a component:
// app/page.tsx
import { listUsers } from "./actions"
export default async function Page() {
const [error, users] = await listUsers({ limit: 10 })
if (error) {
return <p>Error: {error.message}</p>
}
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
)
}The return type
Every action returns a tuple: [error, data].
- On success:
[null, data]—errorisnull,datais the procedure's return value - On failure:
[error, undefined]—errorhascode,status,message, and optionallydata
This pattern avoids try/catch in your components and makes error states explicit.
createActions
Converts an entire router into actions at once. The result mirrors your router structure:
"use server"
import { } from "katman/react"
import { } from "../router"
export const = ()// In a component or server function
const [, ] = await actions.users.list({ : 10 })
const [, ] = await actions.users.create({ : "Alice" })This is convenient when you have many procedures and don't want to export each one individually.
createFormAction
For HTML <form> submissions, createFormAction accepts FormData and converts it into the input object your procedure expects:
// app/actions.ts
"use server"
import { } from "katman/react"
export const = (appRouter.users.create)<form action={submitUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Create User</button>
</form>Nested form data
Bracket notation in field names is automatically parsed into nested objects:
<input name="user[name]" value="Alice" />
<input name="user[email]" value="[email protected]" />Becomes:
{ "user": { "name": "Alice", "email": "[email protected]" } }Array indices work too: items[0], items[1], etc.
Custom parsing
If the default FormData parser doesn't match your needs, provide your own:
const = createFormAction(appRouter.users.create, {
: () => ({
: .get("name"),
: .get("email"),
}),
})Framework errors
Katman automatically detects and rethrows framework-specific errors:
- Next.js
redirect()andnotFound()(errors withNEXT_digest) - TanStack Router navigation errors (
isNotFound: true) - Response objects (used for redirects)
These errors pass through untouched — they won't be caught as [error, data] tuples.
This means redirect("/login") inside a procedure works as expected in Next.js. The redirect happens, and the action doesn't return an error tuple.
useServerAction
A React hook that wraps a server action with loading and error state:
import { useServerAction } from "katman/react"
function CreateUserButton() {
const { execute, data, error, isPending, reset } = useServerAction(createUser)
return (
<div>
<button onClick={() => execute({ name: "Alice" })} disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
{error && <p>Error: {error.message}</p>}
{data && <p>Created: {data.name}</p>}
</div>
)
}execute() returns the same [error, data] tuple as the action itself, so you can also handle results inline.
useOptimisticServerAction
Like useServerAction, but applies an optimistic update immediately while the server call is in flight:
import { useOptimisticServerAction } from "katman/react"
function UserName({ user }) {
const { execute, displayData, isPending } = useOptimisticServerAction(updateUser, {
optimistic: (input) => ({ ...user, ...input }),
})
return (
<div>
<span>{displayData?.name ?? user.name}</span>
<button onClick={() => execute({ name: "Bob" })}>Rename</button>
</div>
)
}displayDatashows the optimistic value while pending, then switches to the confirmed server response.- If the call fails, the optimistic value is rolled back automatically.
What's next?
- Getting Started — set up a Katman router
- Typed Errors — how error maps and
fail()work, and how they appear in theerrortuple - TanStack Query — client-side data fetching with caching, as an alternative