Server Action
React Server Actions let client components invoke asynchronous server functions. With oRPC, you simply append the .actionable modifier to enable Server Action compatibility.
Server Side
Define your procedure with .actionable for Server Action support.
'use server'
import { redirect } from 'next/navigation'
export const ping = os
.input(z.object({ name: z.string() }))
.handler(async ({ input }) => `Hello, ${input.name}`)
.actionable({
context: async () => ({}), // Optional: provide initial context if needed
interceptors: [
onSuccess(async output => redirect(`/some-where`)),
onError(async error => console.error(error)),
],
})TIP
We recommend using Runtime Context instead of Initial Context when working with Server Actions.
Client Side
On the client, import and call your procedure as follows:
'use client'
import { ping } from './actions'
export function MyComponent() {
const [name, setName] = useState('')
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
const [error, data] = await ping({ name })
console.log(error, data)
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
)
}This approach seamlessly integrates server-side procedures with client components via Server Actions.
Type‑Safe Error Handling
The .actionable modifier supports type-safe error handling with a JSON-like error object.
'use client'
const [error, data] = await someAction({ name: 'John' })
if (error) {
if (error.defined) {
console.log(error.data)
// ^ Typed error data
}
// Handle unknown errors
}
else {
// Handle success
console.log(data)
}@orpc/react Package
The @orpc/react package offers utilities to integrate oRPC with React and React Server Actions.
Installation
npm install @orpc/react@latestyarn add @orpc/react@latestpnpm add @orpc/react@latestbun add @orpc/react@latestdeno install npm:@orpc/react@latestuseServerAction Hook
The useServerAction hook simplifies invoking server actions in React.
'use client'
import { useServerAction } from '@orpc/react/hooks'
import { isDefinedError, onError } from '@orpc/client'
export function MyComponent() {
const { execute, data, error, status } = useServerAction(someAction, {
interceptors: [
onError((error) => {
if (isDefinedError(error)) {
console.error(error.data)
// ^ Typed error data
}
}),
],
})
const action = async (form: FormData) => {
const name = form.get('name') as string
execute({ name })
}
return (
<form action={action}>
<input type="text" name="name" required />
<button type="submit">Submit</button>
{status === 'pending' && <p>Loading...</p>}
</form>
)
}createFormAction Utility
The createFormAction utility accepts a procedure and returns a function to handle form submissions. It uses Bracket Notation to deserialize form data.
import { createFormAction } from '@orpc/react'
const dosomething = os
.input(
z.object({
user: z.object({
name: z.string(),
age: z.coerce.number(),
}),
})
)
.handler(({ input }) => {
console.log('Form action called!')
console.log(input)
})
export const redirectSomeWhereForm = createFormAction(dosomething, {
interceptors: [
onSuccess(async () => {
redirect('/some-where')
}),
],
})
export function MyComponent() {
return (
<form action={redirectSomeWhereForm}>
<input type="text" name="user[name]" required />
<input type="number" name="user[age]" required />
<button type="submit">Submit</button>
</form>
)
}By moving the redirect('/some-where') logic into createFormAction rather than the procedure, you enhance the procedure's reusability beyond Server Actions.
INFO
When using createFormAction, any ORPCError with a status of 401, 403, or 404 is automatically converted into the corresponding Next.js error responses: unauthorized, forbidden, and not found.
parseFormData and getIssueMessage utilities
parseFormDataparse a form data with Bracket NotationgetIssueMessageget the standard schema issue message with Bracket Notation path
import { getIssueMessage, parseFormData } from '@orpc/react'
export function MyComponent() {
const { execute, data, error, status } = useServerAction(someAction)
return (
<form action={(form) => { execute(parseFormData(form)) }}>
<label>
Name:
<input name="user[name]" type="text" />
<span>{getIssueMessage(error, 'user[name]')}</span>
</label>
<label>
Age:
<input name="user[age]" type="number" />
<span>{getIssueMessage(error, 'user[age]')}</span>
</label>
<label>
Images:
<input name="images[]" type="file" multiple />
<span>{getIssueMessage(error, 'images[]')}</span>
</label>
<button disabled={status === 'pending'}>
Submit
</button>
</form>
)
}INFO
The getIssueMessage utility works with any data type but requires validation errors to follow the standard schema issue format. It looks for issues in the data.issues property. If you use custom validation errors, store them elsewhere, or modify the issue format, getIssueMessage may not work as expected.
