or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

app-router.mdcache-links.mdindex.mdpages-router.mdserver-actions.mdssr.md

server-actions.mddocs/

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

```