0
# Framework Integrations
1
2
Server-side validation utilities for Next.js, Remix, and TanStack Start frameworks. These integrations enable Progressive Enhancement patterns with server-side validation and client-side hydration.
3
4
## Capabilities
5
6
### Next.js Integration
7
8
Server-side validation for Next.js Server Actions.
9
10
```typescript { .api }
11
/**
12
* Creates a server validation function for Next.js Server Actions
13
* Validates FormData on the server and throws ServerValidateError on failure
14
*
15
* @param defaultOpts - Configuration for server validation
16
* @returns Async function that validates FormData and returns parsed data
17
*/
18
function createServerValidate<
19
TFormData,
20
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
21
>(
22
defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,
23
): (
24
formData: FormData,
25
info?: { resolve?: (fieldName: string) => string | File },
26
) => Promise<TFormData>;
27
28
interface CreateServerValidateOptions<
29
TFormData,
30
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
31
> {
32
/** Default form values */
33
defaultValues?: TFormData;
34
35
/**
36
* Server-side validator function or Standard Schema
37
* Validates the parsed form data before processing
38
*/
39
onServerValidate?: TOnServer;
40
41
/**
42
* Custom parser for FormData
43
* @param formData - Raw FormData from form submission
44
* @returns Parsed form data object
45
*/
46
parse?: (formData: FormData) => TFormData;
47
}
48
49
/**
50
* Initial form state constant
51
* Use this as default state before server validation completes
52
*/
53
const initialFormState: ServerFormState<any, undefined>;
54
55
/**
56
* Server validation error class
57
* Thrown by createServerValidate when validation fails
58
*/
59
class ServerValidateError<
60
TFormData,
61
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
62
> extends Error {
63
/** Form state with validation errors */
64
formState: ServerFormState<TFormData, TOnServer>;
65
66
constructor(formState: ServerFormState<TFormData, TOnServer>);
67
}
68
69
/**
70
* Server form state type
71
* Subset of full FormState used for server-side validation
72
*/
73
type ServerFormState<
74
TFormData,
75
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
76
> = Pick<
77
FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,
78
'values' | 'errors' | 'errorMap'
79
>;
80
```
81
82
**Usage Example:**
83
84
```typescript
85
// app/actions.ts (Next.js Server Action)
86
'use server';
87
88
import { createServerValidate } from '@tanstack/react-form/nextjs';
89
import { z } from 'zod';
90
91
const serverValidate = createServerValidate({
92
defaultValues: {
93
name: '',
94
email: '',
95
},
96
onServerValidate: z.object({
97
name: z.string().min(2),
98
email: z.string().email(),
99
}),
100
});
101
102
export async function submitForm(prev: any, formData: FormData) {
103
try {
104
const data = await serverValidate(formData);
105
106
// Process validated data
107
await saveToDatabase(data);
108
109
return { success: true };
110
} catch (error) {
111
if (error instanceof ServerValidateError) {
112
return error.formState;
113
}
114
throw error;
115
}
116
}
117
118
// app/form.tsx (Client Component)
119
'use client';
120
121
import { useForm } from '@tanstack/react-form';
122
import { useFormState } from 'react-dom';
123
import { submitForm } from './actions';
124
import { initialFormState } from '@tanstack/react-form/nextjs';
125
126
export function MyForm() {
127
const [serverFormState, formAction] = useFormState(submitForm, initialFormState);
128
129
const form = useForm({
130
defaultValues: {
131
name: '',
132
email: '',
133
},
134
// Merge server validation errors with client state
135
defaultState: serverFormState,
136
});
137
138
return (
139
<form action={formAction}>
140
<form.Field name="name">
141
{(field) => (
142
<input
143
name={field.name}
144
value={field.state.value}
145
onChange={(e) => field.handleChange(e.target.value)}
146
/>
147
)}
148
</form.Field>
149
150
<form.Field name="email">
151
{(field) => (
152
<input
153
name={field.name}
154
value={field.state.value}
155
onChange={(e) => field.handleChange(e.target.value)}
156
/>
157
)}
158
</form.Field>
159
160
<button type="submit">Submit</button>
161
</form>
162
);
163
}
164
```
165
166
### Remix Integration
167
168
Server-side validation for Remix Form Actions.
169
170
```typescript { .api }
171
/**
172
* Creates a server validation function for Remix actions
173
* Validates FormData on the server and throws ServerValidateError on failure
174
*
175
* @param defaultOpts - Configuration for server validation
176
* @returns Async function that validates FormData and returns parsed data
177
*/
178
function createServerValidate<
179
TFormData,
180
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
181
>(
182
defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,
183
): (
184
formData: FormData,
185
info?: { resolve?: (fieldName: string) => string | File },
186
) => Promise<TFormData>;
187
188
/**
189
* Initial form state constant
190
* Use this as default state in loader functions
191
*/
192
const initialFormState: ServerFormState<any, undefined>;
193
194
/**
195
* Server validation error class
196
* Thrown by createServerValidate when validation fails
197
*/
198
class ServerValidateError<
199
TFormData,
200
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
201
> extends Error {
202
/** Form state with validation errors */
203
formState: ServerFormState<TFormData, TOnServer>;
204
205
constructor(formState: ServerFormState<TFormData, TOnServer>);
206
}
207
208
/** Server form state type (same as Next.js) */
209
type ServerFormState<
210
TFormData,
211
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
212
> = Pick<
213
FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,
214
'values' | 'errors' | 'errorMap'
215
>;
216
```
217
218
**Usage Example:**
219
220
```typescript
221
// app/routes/contact.tsx (Remix)
222
import { json, type ActionFunctionArgs } from '@remix-run/node';
223
import { useActionData, Form } from '@remix-run/react';
224
import { useForm } from '@tanstack/react-form';
225
import { createServerValidate, initialFormState } from '@tanstack/react-form/remix';
226
import { z } from 'zod';
227
228
const serverValidate = createServerValidate({
229
defaultValues: {
230
name: '',
231
email: '',
232
message: '',
233
},
234
onServerValidate: z.object({
235
name: z.string().min(2),
236
email: z.string().email(),
237
message: z.string().min(10),
238
}),
239
});
240
241
export async function action({ request }: ActionFunctionArgs) {
242
const formData = await request.formData();
243
244
try {
245
const data = await serverValidate(formData);
246
247
// Process validated data
248
await sendEmail(data);
249
250
return json({ success: true });
251
} catch (error) {
252
if (error instanceof ServerValidateError) {
253
return json(error.formState);
254
}
255
throw error;
256
}
257
}
258
259
export default function ContactRoute() {
260
const actionData = useActionData<typeof action>();
261
const serverFormState = actionData?.success ? initialFormState : actionData;
262
263
const form = useForm({
264
defaultValues: {
265
name: '',
266
email: '',
267
message: '',
268
},
269
defaultState: serverFormState,
270
});
271
272
return (
273
<Form method="post">
274
<form.Field name="name">
275
{(field) => (
276
<div>
277
<input
278
name={field.name}
279
value={field.state.value}
280
onChange={(e) => field.handleChange(e.target.value)}
281
/>
282
{field.state.meta.errors[0]}
283
</div>
284
)}
285
</form.Field>
286
287
<form.Field name="email">
288
{(field) => (
289
<div>
290
<input
291
name={field.name}
292
value={field.state.value}
293
onChange={(e) => field.handleChange(e.target.value)}
294
/>
295
{field.state.meta.errors[0]}
296
</div>
297
)}
298
</form.Field>
299
300
<form.Field name="message">
301
{(field) => (
302
<div>
303
<textarea
304
name={field.name}
305
value={field.state.value}
306
onChange={(e) => field.handleChange(e.target.value)}
307
/>
308
{field.state.meta.errors[0]}
309
</div>
310
)}
311
</form.Field>
312
313
<button type="submit">Submit</button>
314
</Form>
315
);
316
}
317
```
318
319
### TanStack Start Integration
320
321
Server-side validation for TanStack Start with cookie-based state persistence.
322
323
```typescript { .api }
324
/**
325
* Creates a server validation function for TanStack Start
326
* Validates FormData and stores state in cookies for post-redirect access
327
*
328
* @param defaultOpts - Configuration for server validation
329
* @returns Async function that validates FormData and returns parsed data
330
*/
331
function createServerValidate<
332
TFormData,
333
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
334
>(
335
defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,
336
): (
337
formData: FormData,
338
info?: { resolve?: (fieldName: string) => string | File },
339
) => Promise<TFormData>;
340
341
/**
342
* Retrieves form data from cookies set by server validation
343
* Use this in your component to access validation state after redirect
344
*
345
* @returns Promise resolving to server form state or initialFormState
346
*/
347
function getFormData(): Promise<
348
ServerFormState<any, undefined> | typeof initialFormState
349
>;
350
351
/**
352
* Initial form state constant
353
*/
354
const initialFormState: {
355
errorMap: { onServer: undefined };
356
errors: [];
357
};
358
359
/**
360
* Server validation error class for TanStack Start
361
* Includes Response object for redirect handling
362
*/
363
class ServerValidateError<
364
TFormData,
365
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
366
> extends Error {
367
/** Form state with validation errors */
368
formState: ServerFormState<TFormData, TOnServer>;
369
370
/** Response object for redirects and headers */
371
response: Response;
372
373
constructor(formState: ServerFormState<TFormData, TOnServer>, response: Response);
374
}
375
376
/** Server form state type */
377
type ServerFormState<
378
TFormData,
379
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
380
> = Pick<
381
FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,
382
'values' | 'errors' | 'errorMap'
383
>;
384
```
385
386
**Usage Example:**
387
388
```typescript
389
// app/routes/contact.tsx (TanStack Start)
390
import { createServerFn } from '@tanstack/start';
391
import { useServerFn } from '@tanstack/start';
392
import { useForm } from '@tanstack/react-form';
393
import {
394
createServerValidate,
395
getFormData,
396
initialFormState,
397
ServerValidateError,
398
} from '@tanstack/react-form/start';
399
import { z } from 'zod';
400
401
const serverValidate = createServerValidate({
402
defaultValues: {
403
name: '',
404
email: '',
405
},
406
onServerValidate: z.object({
407
name: z.string().min(2),
408
email: z.string().email(),
409
}),
410
});
411
412
const submitFormAction = createServerFn({ method: 'POST' })
413
.validator((formData: FormData) => formData)
414
.handler(async ({ data }) => {
415
try {
416
const validated = await serverValidate(data);
417
418
// Process validated data
419
await saveToDatabase(validated);
420
421
return { success: true };
422
} catch (error) {
423
if (error instanceof ServerValidateError) {
424
// Throw the response with cookies
425
throw error.response;
426
}
427
throw error;
428
}
429
});
430
431
export default function ContactRoute() {
432
const serverFormState = getFormData();
433
const submitForm = useServerFn(submitFormAction);
434
435
const form = useForm({
436
defaultValues: {
437
name: '',
438
email: '',
439
},
440
defaultState: serverFormState,
441
onSubmit: async ({ value }) => {
442
const formData = new FormData();
443
formData.append('name', value.name);
444
formData.append('email', value.email);
445
446
await submitForm({ data: formData });
447
},
448
});
449
450
return (
451
<form
452
onSubmit={(e) => {
453
e.preventDefault();
454
form.handleSubmit();
455
}}
456
>
457
<form.Field name="name">
458
{(field) => (
459
<div>
460
<input
461
value={field.state.value}
462
onChange={(e) => field.handleChange(e.target.value)}
463
/>
464
{field.state.meta.errors[0]}
465
</div>
466
)}
467
</form.Field>
468
469
<form.Field name="email">
470
{(field) => (
471
<div>
472
<input
473
value={field.state.value}
474
onChange={(e) => field.handleChange(e.target.value)}
475
/>
476
{field.state.meta.errors[0]}
477
</div>
478
)}
479
</form.Field>
480
481
<button type="submit">Submit</button>
482
</form>
483
);
484
}
485
```
486
487
## Import Paths
488
489
```typescript
490
// Next.js
491
import {
492
createServerValidate,
493
initialFormState,
494
ServerValidateError,
495
type ServerFormState,
496
} from '@tanstack/react-form/nextjs';
497
498
// Remix
499
import {
500
createServerValidate,
501
initialFormState,
502
ServerValidateError,
503
type ServerFormState,
504
} from '@tanstack/react-form/remix';
505
506
// TanStack Start
507
import {
508
createServerValidate,
509
getFormData,
510
initialFormState,
511
ServerValidateError,
512
type ServerFormState,
513
} from '@tanstack/react-form/start';
514
```
515
516
## Common Patterns
517
518
### Progressive Enhancement
519
520
All framework integrations support Progressive Enhancement where forms work without JavaScript:
521
522
```typescript
523
// The form submits to server even without JS
524
<form action={formAction} method="post">
525
{/* name attribute required for server-side parsing */}
526
<input name="email" />
527
<button type="submit">Submit</button>
528
</form>
529
530
// With JS, client-side validation enhances UX
531
<form
532
action={formAction}
533
onSubmit={(e) => {
534
e.preventDefault();
535
form.handleSubmit();
536
}}
537
>
538
<form.Field name="email">
539
{(field) => (
540
<input
541
name={field.name}
542
value={field.state.value}
543
onChange={(e) => field.handleChange(e.target.value)}
544
/>
545
)}
546
</form.Field>
547
</form>
548
```
549
550
### Custom FormData Parsing
551
552
```typescript
553
const serverValidate = createServerValidate({
554
defaultValues: {
555
tags: [],
556
metadata: {},
557
},
558
parse: (formData) => {
559
return {
560
tags: formData.getAll('tags'),
561
metadata: JSON.parse(formData.get('metadata') as string),
562
};
563
},
564
onServerValidate: (data) => {
565
if (data.tags.length === 0) {
566
return 'At least one tag required';
567
}
568
return undefined;
569
},
570
});
571
```
572
573
### File Upload Handling
574
575
```typescript
576
const serverValidate = createServerValidate({
577
defaultValues: {
578
file: null as File | null,
579
},
580
parse: (formData) => ({
581
file: formData.get('file') as File,
582
}),
583
onServerValidate: async ({ value }) => {
584
if (!value.file) {
585
return { fields: { file: 'File is required' } };
586
}
587
588
if (value.file.size > 5 * 1024 * 1024) {
589
return { fields: { file: 'File must be less than 5MB' } };
590
}
591
592
return undefined;
593
},
594
});
595
```
596
597
### Shared Validation Between Client and Server
598
599
```typescript
600
// shared/validation.ts
601
import { z } from 'zod';
602
603
export const contactSchema = z.object({
604
name: z.string().min(2, 'Name must be at least 2 characters'),
605
email: z.string().email('Invalid email address'),
606
message: z.string().min(10, 'Message must be at least 10 characters'),
607
});
608
609
// server/action.ts
610
import { createServerValidate } from '@tanstack/react-form/nextjs';
611
import { contactSchema } from '../shared/validation';
612
613
export const serverValidate = createServerValidate({
614
defaultValues: {
615
name: '',
616
email: '',
617
message: '',
618
},
619
onServerValidate: contactSchema,
620
});
621
622
// client/form.tsx
623
import { useForm } from '@tanstack/react-form';
624
import { contactSchema } from '../shared/validation';
625
626
export function ContactForm() {
627
const form = useForm({
628
defaultValues: {
629
name: '',
630
email: '',
631
message: '',
632
},
633
validators: {
634
onChange: contactSchema,
635
},
636
});
637
638
return <form>{/* fields */}</form>;
639
}
640
```
641