0
# Server Actions
1
2
Experimental server actions integration for form handling and mutations in Next.js App Router.
3
4
## Capabilities
5
6
### Action Hook Creation
7
8
Create React hooks for handling tRPC server actions with loading states and error handling.
9
10
```typescript { .api }
11
/**
12
* Creates a React hook factory for tRPC server actions
13
* @param opts - tRPC client configuration options
14
* @returns Function that creates action hooks for specific handlers
15
*/
16
function experimental_createActionHook<TInferrable extends InferrableClientTypes>(
17
opts: CreateTRPCClientOptions<TInferrable>
18
): <TDef extends ActionHandlerDef>(
19
handler: TRPCActionHandler<TDef>,
20
useActionOpts?: UseTRPCActionOptions<TDef>
21
) => UseTRPCActionResult<TDef>;
22
23
interface UseTRPCActionOptions<TDef extends ActionHandlerDef> {
24
/** Callback called on successful action completion */
25
onSuccess?: (result: TDef['output']) => MaybePromise<void> | void;
26
/** Callback called on action error */
27
onError?: (result: TRPCClientError<TDef['errorShape']>) => MaybePromise<void>;
28
}
29
```
30
31
**Usage Examples:**
32
33
```typescript
34
import { experimental_createActionHook, experimental_serverActionLink } from "@trpc/next/app-dir/client";
35
36
// Create the action hook factory
37
const useAction = experimental_createActionHook({
38
links: [experimental_serverActionLink()],
39
});
40
41
// Use in a component
42
function CreatePostForm() {
43
const createPost = useAction(createPostAction, {
44
onSuccess: (result) => {
45
console.log("Post created:", result.id);
46
},
47
onError: (error) => {
48
console.error("Failed to create post:", error.message);
49
},
50
});
51
52
const handleSubmit = (formData: FormData) => {
53
createPost.mutate(formData);
54
};
55
56
return (
57
<form action={handleSubmit}>
58
<input name="title" placeholder="Post title" />
59
<textarea name="content" placeholder="Post content" />
60
<button type="submit" disabled={createPost.status === "loading"}>
61
{createPost.status === "loading" ? "Creating..." : "Create Post"}
62
</button>
63
{createPost.error && <p>Error: {createPost.error.message}</p>}
64
</form>
65
);
66
}
67
```
68
69
### Server Action Handler Creation
70
71
Create server action handlers that integrate with tRPC procedures.
72
73
```typescript { .api }
74
/**
75
* Creates server action handlers that integrate with tRPC procedures
76
* @param t - tRPC instance with configuration
77
* @param opts - Context creation and error handling options
78
* @returns Function that creates action handlers for specific procedures
79
*/
80
function experimental_createServerActionHandler<TInstance extends { _config: RootConfig<AnyRootTypes> }>(
81
t: TInstance,
82
opts: CreateContextCallback<TInstance['_config']['$types']['ctx'], () => MaybePromise<TInstance['_config']['$types']['ctx']>> & {
83
/** Transform form data to a Record before passing it to the procedure (default: true) */
84
normalizeFormData?: boolean;
85
/** Called when an error occurs in the handler */
86
onError?: (opts: ErrorHandlerOptions<TInstance['_config']['$types']['ctx']>) => void;
87
/** Rethrow errors that should be handled by Next.js (default: true) */
88
rethrowNextErrors?: boolean;
89
}
90
): <TProc extends AnyProcedure>(proc: TProc) => TRPCActionHandler<inferActionDef<TInstance, TProc>>;
91
92
type TRPCActionHandler<TDef extends ActionHandlerDef> = (
93
input: FormData | TDef['input']
94
) => Promise<TRPCResponse<TDef['output'], TDef['errorShape']>>;
95
```
96
97
**Usage Examples:**
98
99
```typescript
100
import { experimental_createServerActionHandler } from "@trpc/next/app-dir/server";
101
import { z } from "zod";
102
103
// Initialize tRPC
104
const t = initTRPC.context<{ userId?: string }>().create();
105
const procedure = t.procedure;
106
107
// Create action handler factory
108
const createAction = experimental_createServerActionHandler(t, {
109
createContext: async () => {
110
// Get user from session, database, etc.
111
return { userId: "user123" };
112
},
113
onError: ({ error, ctx }) => {
114
console.error("Action error:", error, "Context:", ctx);
115
},
116
});
117
118
// Define a procedure
119
const createPostProcedure = procedure
120
.input(z.object({
121
title: z.string(),
122
content: z.string(),
123
}))
124
.mutation(async ({ input, ctx }) => {
125
// Your mutation logic here
126
return { id: "post123", ...input };
127
});
128
129
// Create the action handler
130
const createPostAction = createAction(createPostProcedure);
131
132
// Use in server component or route handler
133
export async function createPost(formData: FormData) {
134
"use server";
135
return createPostAction(formData);
136
}
137
```
138
139
### Server Action Link
140
141
tRPC link that handles communication between client action hooks and server actions.
142
143
```typescript { .api }
144
/**
145
* tRPC link that handles communication with server actions
146
* @param opts - Optional transformer configuration
147
* @returns tRPC link for server action communication
148
*/
149
function experimental_serverActionLink<TInferrable extends InferrableClientTypes>(
150
opts?: TransformerOptions<inferClientTypes<TInferrable>>
151
): TRPCLink<TInferrable>;
152
```
153
154
**Usage Examples:**
155
156
```typescript
157
import { experimental_serverActionLink } from "@trpc/next/app-dir/client";
158
import superjson from "superjson";
159
160
// Basic usage
161
const basicLink = experimental_serverActionLink();
162
163
// With transformer
164
const linkWithTransformer = experimental_serverActionLink({
165
transformer: superjson,
166
});
167
168
// Use in client configuration
169
const useAction = experimental_createActionHook({
170
links: [linkWithTransformer],
171
transformer: superjson,
172
});
173
```
174
175
### Action Result States
176
177
The action hook returns different states based on the current status of the action.
178
179
```typescript { .api }
180
type UseTRPCActionResult<TDef extends ActionHandlerDef> =
181
| UseTRPCActionErrorResult<TDef>
182
| UseTRPCActionIdleResult<TDef>
183
| UseTRPCActionLoadingResult<TDef>
184
| UseTRPCActionSuccessResult<TDef>;
185
186
interface UseTRPCActionBaseResult<TDef extends ActionHandlerDef> {
187
mutate: (...args: MutationArgs<TDef>) => void;
188
mutateAsync: (...args: MutationArgs<TDef>) => Promise<TDef['output']>;
189
}
190
191
interface UseTRPCActionSuccessResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
192
data: TDef['output'];
193
error?: never;
194
status: 'success';
195
}
196
197
interface UseTRPCActionErrorResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
198
data?: never;
199
error: TRPCClientError<TDef['errorShape']>;
200
status: 'error';
201
}
202
203
interface UseTRPCActionIdleResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
204
data?: never;
205
error?: never;
206
status: 'idle';
207
}
208
209
interface UseTRPCActionLoadingResult<TDef extends ActionHandlerDef> extends UseTRPCActionBaseResult<TDef> {
210
data?: never;
211
error?: never;
212
status: 'loading';
213
}
214
```
215
216
## Advanced Usage
217
218
### Form Data Handling
219
220
Server actions can handle both FormData and typed objects.
221
222
```typescript
223
// Server action that handles FormData
224
const createAction = experimental_createServerActionHandler(t, {
225
createContext: async () => ({}),
226
normalizeFormData: true, // Convert FormData to object
227
});
228
229
const signupProcedure = procedure
230
.input(z.object({
231
email: z.string().email(),
232
password: z.string().min(8),
233
terms: z.string().optional(),
234
}))
235
.mutation(async ({ input }) => {
236
// FormData is automatically converted to typed object
237
const user = await createUser({
238
email: input.email,
239
password: input.password,
240
acceptedTerms: input.terms === "on",
241
});
242
return user;
243
});
244
245
const signupAction = createAction(signupProcedure);
246
247
// Client component
248
function SignupForm() {
249
const signup = useAction(signupAction);
250
251
return (
252
<form action={signup.mutate}>
253
<input name="email" type="email" required />
254
<input name="password" type="password" required />
255
<input name="terms" type="checkbox" />
256
<button type="submit">Sign Up</button>
257
</form>
258
);
259
}
260
```
261
262
### Error Handling
263
264
Comprehensive error handling with different error types.
265
266
```typescript
267
const createAction = experimental_createServerActionHandler(t, {
268
createContext: async () => ({}),
269
onError: ({ error, ctx, input, path, type }) => {
270
// Log errors for monitoring
271
console.error("Server action error:", {
272
path,
273
type,
274
error: error.message,
275
code: error.code,
276
input,
277
});
278
279
// Send to error tracking service
280
if (error.code === "INTERNAL_SERVER_ERROR") {
281
sendToErrorTracking(error);
282
}
283
},
284
rethrowNextErrors: true, // Let Next.js handle redirect/notFound errors
285
});
286
287
// Client with error handling
288
function CreatePostForm() {
289
const createPost = useAction(createPostAction, {
290
onError: async (error) => {
291
if (error.data?.code === "UNAUTHORIZED") {
292
// Redirect to login
293
window.location.href = "/login";
294
} else {
295
// Show user-friendly error
296
toast.error("Failed to create post. Please try again.");
297
}
298
},
299
});
300
301
// Component implementation...
302
}
303
```
304
305
### Progressive Enhancement
306
307
Server actions work without JavaScript, providing progressive enhancement.
308
309
```typescript
310
// Server component with progressive enhancement
311
function PostForm({ post }: { post?: Post }) {
312
return (
313
<form action={createOrUpdatePostAction}>
314
{post && <input type="hidden" name="id" value={post.id} />}
315
<input name="title" defaultValue={post?.title} required />
316
<textarea name="content" defaultValue={post?.content} required />
317
<button type="submit">
318
{post ? "Update" : "Create"} Post
319
</button>
320
</form>
321
);
322
}
323
324
// Enhanced client component
325
"use client";
326
function EnhancedPostForm({ post }: { post?: Post }) {
327
const savePost = useAction(createOrUpdatePostAction, {
328
onSuccess: () => {
329
toast.success("Post saved!");
330
router.refresh();
331
},
332
});
333
334
return (
335
<form action={savePost.mutate}>
336
{/* Form fields... */}
337
<button type="submit" disabled={savePost.status === "loading"}>
338
{savePost.status === "loading" ? "Saving..." : "Save Post"}
339
</button>
340
</form>
341
);
342
}
343
```
344
345
## Types
346
347
```typescript { .api }
348
// Action handler definition
349
interface ActionHandlerDef {
350
input?: any;
351
output?: any;
352
errorShape: any;
353
}
354
355
// Mutation arguments based on input type
356
type MutationArgs<TDef extends ActionHandlerDef> = TDef['input'] extends void
357
? [input?: undefined | void, opts?: TRPCProcedureOptions]
358
: [input: FormData | TDef['input'], opts?: TRPCProcedureOptions];
359
360
// Error handler options
361
interface ErrorHandlerOptions<TContext> {
362
ctx: TContext | undefined;
363
error: TRPCError;
364
input: unknown;
365
path: string;
366
type: ProcedureType;
367
}
368
369
// tRPC response type
370
interface TRPCResponse<TData, TError> {
371
result?: {
372
data: TData;
373
};
374
error?: TError;
375
}
376
```