oRPC Server Action
Integrate oRPC procedures with React Server Actions.
Файлы2
Открыть в GitHubSKILL.mdskills/orpc-server-action/SKILL.md
GitHubServer Action
React Server Actions let client components invoke async server functions. With oRPC, append .actionable to enable Server Action compatibility.
Server Side
'use server'
import { redirect } from 'next/navigation'
import { onError, onSuccess, os } from '@orpc/server'
import * as z from 'zod'
export const ping = os
.input(z.object({ name: z.string() }))
.handler(async ({ input }) => `Hello, ${input.name}`)
.actionable({
context: async () => ({}),
interceptors: [
onSuccess(async output => redirect(`/some-where`)),
onError(async error => console.error(error)),
],
})
Use Execution Context instead of Initial Context.
Special errors (
redirect,notFound) are only supported in Next.js and TanStack Start.
Client Side
'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>
)
}
Type-Safe Error Handling
export const someAction = os
.input(z.object({ name: z.string() }))
.errors({
SOME_ERROR: {
message: 'Some error message',
data: z.object({ some: z.string() }),
},
})
.handler(async ({ input }) => `Hello, ${input.name}`)
.actionable()
'use client'
const [error, data] = await someAction({ name: 'John' })
if (error) {
if (error.defined) {
console.log(error.data) // Typed error data
}
} else {
console.log(data)
}
@orpc/react Package
Installation
npm install @orpc/react@latest
useServerAction Hook
'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)
}
}),
],
})
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>
)
}
useOptimisticServerAction Hook
import { useOptimisticServerAction } from '@orpc/react/hooks'
import { onSuccessDeferred } from '@orpc/react'
export function MyComponent() {
const [todos, setTodos] = useState<Todo[]>([])
const { execute, optimisticState } = useOptimisticServerAction(someAction, {
optimisticPassthrough: todos,
optimisticReducer: (currentState, newTodo) => [...currentState, newTodo],
interceptors: [
onSuccessDeferred(({ data }) => {
setTodos(prevTodos => [...prevTodos, data])
}),
],
})
const handleSubmit = (form: FormData) => {
const todo = form.get('todo') as string
execute({ todo })
}
return (
<div>
<ul>
{optimisticState.map(todo => <li key={todo.todo}>{todo.todo}</li>)}
</ul>
<form action={handleSubmit}>
<input type="text" name="todo" required />
<button type="submit">Add Todo</button>
</form>
</div>
)
}
createFormAction Utility
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!', 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>
)
}
ORPCErrorwith status401/403/404is automatically converted to Next.js errors.
Form Data Utilities
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>
<button disabled={status === 'pending'}>Submit</button>
</form>
)
}