Skip to content

Commit 9486ab5

Browse files
authored
feat(server, react)!: remake server action (#308)
Entirely rewrite server action for oRPC and comewith some utilities and hooks for react <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a dedicated React integration package that delivers utilities for executing server actions and handling form submissions. - Enabled experimental support in Next.js for enhanced authentication handling. - **Documentation** - Updated guides and package overviews to highlight the new React integration capabilities. - **Chores** - Refreshed dependencies and configurations for Next.js and React. - **Tests** - Expanded test coverage to validate the new integration utilities and enhanced workflows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 54ba24c commit 9486ab5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1567
-165
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ You can find the full documentation [here](https://orpc.unnoq.com).
5353
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
5454
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
5555
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
56+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
5657
- [@orpc/react-query](https://www.npmjs.com/package/@orpc/react-query): Integration with [React Query](https://tanstack.com/query/latest/docs/framework/react/overview).
5758
- [@orpc/vue-query](https://www.npmjs.com/package/@orpc/vue-query): Integration with [Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview).
5859
- [@orpc/solid-query](https://www.npmjs.com/package/@orpc/solid-query): Integration with [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview).

apps/content/docs/procedure.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const example = os
2828
return { id: 1 }
2929
})
3030
.callable() // Make the procedure callable like a regular function
31-
.actionable() // Like .callable, but compatible with server actions
31+
.actionable() // Server Action compatibility
3232
```
3333

3434
> The `.handler` method is the only required step. All other chains are optional.

apps/content/docs/server-action.md

+177-19
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,36 @@ description: Integrate oRPC procedures with React Server Actions
55

66
# Server Action
77

8-
[Server Action](https://react.dev/reference/rsc/server-functions) let client components call asynchronous server functions. With oRPC, you can make your procedures compatible by appending the `.actionable` modifier.
8+
React [Server Actions](https://react.dev/reference/rsc/server-functions) let client components invoke asynchronous server functions. With oRPC, you simply append the `.actionable` modifier to enable Server Action compatibility.
99

1010
## Server Side
1111

12-
Define your procedure using `.actionable` to enable Server Action compatibility. For example:
12+
Define your procedure with `.actionable` for Server Action support.
1313

1414
```ts twoslash
1515
import { onError, os } from '@orpc/server'
1616
import { z } from 'zod'
17+
// ---cut---
1718
'use server'
1819

1920
export const ping = os
2021
.input(z.object({ name: z.string() }))
21-
.handler(async ({ input }) => {
22-
return `Hello, ${input.name}`
23-
})
22+
.handler(async ({ input }) => `Hello, ${input.name}`)
2423
.actionable({
25-
context: async () => ({}), // Provide initial context if needed
24+
context: async () => ({}), // Optional: provide initial context if needed
2625
interceptors: [
27-
onError((error) => {
28-
console.error(error)
29-
}),
30-
]
26+
onError(error => console.error(error)),
27+
],
3128
})
3229
```
3330

3431
:::tip
35-
When using Server Actions, we recommend [Runtime Context](/docs/context#execution-context) over [Initial Context](/docs/context#initial-context).
32+
We recommend using [Runtime Context](/docs/context#execution-context) instead of [Initial Context](/docs/context#initial-context) when working with Server Actions.
3633
:::
3734

3835
## Client Side
3936

40-
On the client, simply import and call your procedure as shown below:
37+
On the client, import and call your procedure as follows:
4138

4239
```tsx
4340
'use client'
@@ -49,20 +46,181 @@ export function MyComponent() {
4946

5047
const handleSubmit = async (e: FormEvent) => {
5148
e.preventDefault()
52-
const result = await ping({ name })
53-
console.log(result)
49+
const [error, data] = await ping({ name })
50+
console.log(error, data)
5451
}
5552

5653
return (
5754
<form onSubmit={handleSubmit}>
58-
<input
59-
value={name}
60-
onChange={e => setName(e.target.value)}
61-
/>
55+
<input value={name} onChange={e => setName(e.target.value)} />
56+
<button type="submit">Submit</button>
57+
</form>
58+
)
59+
}
60+
```
61+
62+
This approach seamlessly integrates server-side procedures with client components via Server Actions.
63+
64+
## Type‑Safe Error Handling
65+
66+
The `.actionable` modifier supports type-safe error handling with a JSON-like error object.
67+
68+
```ts twoslash
69+
import { os } from '@orpc/server'
70+
import { z } from 'zod'
71+
72+
export const someAction = os
73+
.input(z.object({ name: z.string() }))
74+
.errors({
75+
SOME_ERROR: {
76+
message: 'Some error message',
77+
data: z.object({ some: z.string() }),
78+
},
79+
})
80+
.handler(async ({ input }) => `Hello, ${input.name}`)
81+
.actionable()
82+
// ---cut---
83+
'use client'
84+
85+
const [error, data] = await someAction({ name: 'John' })
86+
87+
if (error) {
88+
if (error.defined) {
89+
console.log(error.data)
90+
// ^ Typed error data
91+
}
92+
// Handle unknown errors
93+
}
94+
else {
95+
// Handle success
96+
console.log(data)
97+
}
98+
```
99+
100+
## `@orpc/react` Package
101+
102+
The `@orpc/react` package offers utilities to integrate oRPC with React and React Server Actions.
103+
104+
### Installation
105+
106+
::: code-group
107+
108+
```sh [npm]
109+
npm install @orpc/react@latest
110+
```
111+
112+
```sh [yarn]
113+
yarn add @orpc/react@latest
114+
```
115+
116+
```sh [pnpm]
117+
pnpm add @orpc/react@latest
118+
```
119+
120+
```sh [bun]
121+
bun add @orpc/react@latest
122+
```
123+
124+
```sh [deno]
125+
deno install npm:@orpc/react@latest
126+
```
127+
128+
:::
129+
130+
### `useServerAction` Hook
131+
132+
The `useServerAction` hook simplifies invoking server actions in React.
133+
134+
```tsx twoslash
135+
import * as React from 'react'
136+
import { os } from '@orpc/server'
137+
import { z } from 'zod'
138+
139+
export const someAction = os
140+
.input(z.object({ name: z.string() }))
141+
.errors({
142+
SOME_ERROR: {
143+
message: 'Some error message',
144+
data: z.object({ some: z.string() }),
145+
},
146+
})
147+
.handler(async ({ input }) => `Hello, ${input.name}`)
148+
.actionable()
149+
// ---cut---
150+
'use client'
151+
152+
import { useServerAction } from '@orpc/react/hooks'
153+
import { isDefinedError, onError } from '@orpc/client'
154+
155+
export function MyComponent() {
156+
const { execute, data, error, status } = useServerAction(someAction, {
157+
interceptors: [
158+
onError((error) => {
159+
if (isDefinedError(error)) {
160+
console.error(error.data)
161+
// ^ Typed error data
162+
}
163+
}),
164+
],
165+
})
166+
167+
const action = async (form: FormData) => {
168+
const name = form.get('name') as string
169+
execute({ name })
170+
}
171+
172+
return (
173+
<form action={action}>
174+
<input type="text" name="name" required />
62175
<button type="submit">Submit</button>
176+
{status === 'pending' && <p>Loading...</p>}
63177
</form>
64178
)
65179
}
66180
```
67181

68-
This approach seamlessly integrates server-side procedures with your client components using Server Actions.
182+
### `createFormAction` Utility
183+
184+
The `createFormAction` utility accepts a [procedure](/docs/procedure) and returns a function to handle form submissions. It uses [Bracket Notation](/docs/openapi/bracket-notation) to deserialize form data.
185+
186+
```tsx
187+
import { createFormAction } from '@orpc/react'
188+
189+
const dosomething = os
190+
.input(
191+
z.object({
192+
user: z.object({
193+
name: z.string(),
194+
age: z.coerce.number(),
195+
}),
196+
})
197+
)
198+
.handler(({ input }) => {
199+
console.log('Form action called!')
200+
console.log(input)
201+
})
202+
203+
export const redirectSomeWhereForm = createFormAction(dosomething, {
204+
interceptors: [
205+
onSuccess(async () => {
206+
redirect('/some-where')
207+
}),
208+
],
209+
})
210+
211+
export function MyComponent() {
212+
return (
213+
<form action={redirectSomeWhereForm}>
214+
<input type="text" name="user[name]" required />
215+
<input type="number" name="user[age]" required />
216+
<button type="submit">Submit</button>
217+
</form>
218+
)
219+
}
220+
```
221+
222+
By moving the `redirect('/some-where')` logic into `createFormAction` rather than the procedure, you enhance the procedure's reusability beyond Server Actions.
223+
224+
::: info
225+
When using `createFormAction`, any `ORPCError` with a status of `401`, `403`, or `404` is automatically converted into the corresponding Next.js error responses: [unauthorized](https://nextjs.org/docs/app/api-reference/functions/unauthorized), [forbidden](https://nextjs.org/docs/app/api-reference/functions/forbidden), and [not found](https://nextjs.org/docs/app/api-reference/functions/not-found).
226+
:::

apps/content/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@orpc/contract": "workspace:*",
1414
"@orpc/openapi": "workspace:*",
1515
"@orpc/openapi-client": "workspace:*",
16+
"@orpc/react": "workspace:*",
1617
"@orpc/react-query": "workspace:*",
1718
"@orpc/server": "workspace:*",
1819
"@orpc/valibot": "workspace:*",
@@ -24,7 +25,7 @@
2425
"@tanstack/solid-query": "^5.66.4",
2526
"@tanstack/svelte-query": "^5.66.4",
2627
"@tanstack/vue-query": "^5.66.4",
27-
"next": "^15.1.7",
28+
"next": "^15.2.4",
2829
"openai": "^4.85.4",
2930
"pinia": "^2.3.0",
3031
"svelte": "^5.0.0"

packages/arktype/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ You can find the full documentation [here](https://orpc.unnoq.com).
5353
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
5454
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
5555
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
56+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
5657
- [@orpc/react-query](https://www.npmjs.com/package/@orpc/react-query): Integration with [React Query](https://tanstack.com/query/latest/docs/framework/react/overview).
5758
- [@orpc/vue-query](https://www.npmjs.com/package/@orpc/vue-query): Integration with [Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview).
5859
- [@orpc/solid-query](https://www.npmjs.com/package/@orpc/solid-query): Integration with [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview).

packages/client/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ You can find the full documentation [here](https://orpc.unnoq.com).
5353
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
5454
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
5555
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
56+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
5657
- [@orpc/react-query](https://www.npmjs.com/package/@orpc/react-query): Integration with [React Query](https://tanstack.com/query/latest/docs/framework/react/overview).
5758
- [@orpc/vue-query](https://www.npmjs.com/package/@orpc/vue-query): Integration with [Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview).
5859
- [@orpc/solid-query](https://www.npmjs.com/package/@orpc/solid-query): Integration with [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview).

packages/client/src/types.test-d.ts

+29-29
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,44 @@
1+
import type { ORPCError } from './error'
12
import type { Client, ClientContext } from './types'
23

34
describe('client', () => {
4-
const fn: Client<ClientContext, string, number, Error> = async (...[input, options]) => {
5-
expectTypeOf(input).toEqualTypeOf<string>()
6-
expectTypeOf(options).toMatchTypeOf<({ context?: unknown, signal?: AbortSignal }) | undefined>()
7-
return 123
8-
}
9-
10-
const fnWithOptionalInput: Client<ClientContext, string | undefined, number, Error> = async (...args) => {
5+
const client: Client<{ cache?: boolean }, string | undefined, number, Error | ORPCError<'OVERRIDE', unknown>> = async (...args) => {
116
const [input, options] = args
127

138
expectTypeOf(input).toEqualTypeOf<string | undefined>()
14-
expectTypeOf(options).toMatchTypeOf<{ context?: unknown, signal?: AbortSignal } | undefined>()
9+
expectTypeOf(options).toMatchTypeOf<{ context?: { cache?: boolean }, signal?: AbortSignal } | undefined>()
1510
return 123
1611
}
1712

1813
it('just a function', () => {
19-
expectTypeOf(fn).toMatchTypeOf<(input: string, options: { context?: ClientContext, signal?: AbortSignal }) => Promise<number>>()
20-
expectTypeOf(fnWithOptionalInput).toMatchTypeOf<(input: string | undefined, options: { context?: ClientContext, signal?: AbortSignal }) => Promise<number>>()
14+
expectTypeOf(client).toMatchTypeOf<(input: string | undefined, options: { context?: ClientContext, signal?: AbortSignal }) => Promise<number>>()
2115
})
2216

2317
it('infer correct input', () => {
24-
fn('123')
25-
fnWithOptionalInput('123')
18+
client('123')
19+
client(undefined)
20+
client()
2621

2722
// @ts-expect-error - invalid input
28-
fn(123)
23+
client(123)
2924
// @ts-expect-error - invalid input
30-
fnWithOptionalInput(123)
25+
client({})
26+
})
3127

28+
it('require non-undefindable input', () => {
29+
const client = {} as Client<ClientContext, { val: string }, { val: number }, Error>
30+
31+
client({ val: '123' })
32+
// @ts-expect-error - missing input
33+
client()
3234
// @ts-expect-error - invalid input
33-
fn({})
34-
// @ts-expect-error - invalid input
35-
fnWithOptionalInput({})
35+
client({ val: 123 })
3636
})
3737

3838
it('accept signal', () => {
39-
fn('123', { signal: new AbortSignal() })
40-
fnWithOptionalInput('123', { signal: new AbortSignal() })
41-
39+
client('123', { signal: new AbortSignal() })
4240
// @ts-expect-error - invalid signal
43-
fn('123', { signal: 1234 })
44-
// @ts-expect-error - invalid signal
45-
fnWithOptionalInput('123', { signal: 1234 })
46-
})
47-
48-
it('can accept call without args', () => {
49-
expectTypeOf(fnWithOptionalInput()).toMatchTypeOf<Promise<number>>()
50-
// @ts-expect-error - input is required
51-
expectTypeOf(fn()).toEqualTypeOf<Promise<number>>()
41+
client('123', { signal: 1234 })
5242
})
5343

5444
describe('context', () => {
@@ -81,4 +71,14 @@ describe('client', () => {
8171
client({ val: '123' }, { context: { userId: 123 } })
8272
})
8373
})
74+
75+
it('infer correct output', async () => {
76+
expectTypeOf(await client('123')).toEqualTypeOf<number>()
77+
})
78+
79+
it('can reverse infer', () => {
80+
expectTypeOf<
81+
typeof client extends Client<infer C, infer I, infer O, infer E> ? [C, I, O, E] : never
82+
>().toEqualTypeOf<[{ cache?: boolean }, string | undefined, number, Error | ORPCError<'OVERRIDE', unknown>]>()
83+
})
8484
})

packages/contract/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ You can find the full documentation [here](https://orpc.unnoq.com).
5353
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
5454
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
5555
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
56+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
5657
- [@orpc/react-query](https://www.npmjs.com/package/@orpc/react-query): Integration with [React Query](https://tanstack.com/query/latest/docs/framework/react/overview).
5758
- [@orpc/vue-query](https://www.npmjs.com/package/@orpc/vue-query): Integration with [Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview).
5859
- [@orpc/solid-query](https://www.npmjs.com/package/@orpc/solid-query): Integration with [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview).

packages/openapi-client/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ You can find the full documentation [here](https://orpc.unnoq.com).
5353
- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract.
5454
- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract.
5555
- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety.
56+
- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions.
5657
- [@orpc/react-query](https://www.npmjs.com/package/@orpc/react-query): Integration with [React Query](https://tanstack.com/query/latest/docs/framework/react/overview).
5758
- [@orpc/vue-query](https://www.npmjs.com/package/@orpc/vue-query): Integration with [Vue Query](https://tanstack.com/query/latest/docs/framework/vue/overview).
5859
- [@orpc/solid-query](https://www.npmjs.com/package/@orpc/solid-query): Integration with [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview).

0 commit comments

Comments
 (0)