0
# Validation System
1
2
Comprehensive validation system supporting synchronous and asynchronous validators, Standard Schema integration for third-party validation libraries, custom validation logic, and detailed error handling with multiple validation triggers.
3
4
## Capabilities
5
6
### Standard Schema Integration
7
8
TanStack React Form implements the Standard Schema specification for validation library integration.
9
10
```typescript { .api }
11
/**
12
* Standard Schema validator interface (v1)
13
* Implemented by validation libraries like Zod, Yup, Valibot, Joi, etc.
14
*/
15
interface StandardSchemaV1<Input = unknown, Output = Input> {
16
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
17
}
18
19
namespace StandardSchemaV1 {
20
interface Props<Input, Output> {
21
/** Standard Schema version */
22
readonly version: 1;
23
24
/** Library vendor identifier */
25
readonly vendor: string;
26
27
/**
28
* Validation function
29
* @param value - Value to validate
30
* @returns Validation result with output value or issues
31
*/
32
readonly validate: (
33
value: unknown,
34
) => Result<Output> | Promise<Result<Output>>;
35
}
36
37
interface Result<Output> {
38
/** Validated and transformed output value (present if valid) */
39
readonly value?: Output;
40
41
/** Validation issues/errors (present if invalid) */
42
readonly issues?: ReadonlyArray<StandardSchemaV1Issue>;
43
}
44
}
45
46
/**
47
* Standard Schema issue/error interface
48
*/
49
interface StandardSchemaV1Issue {
50
/** Human-readable error message */
51
readonly message: string;
52
53
/** Path to the field with error (e.g., 'user.email') */
54
readonly path?: ReadonlyArray<string | number | symbol>;
55
}
56
57
/**
58
* Type guard to check if a validator is a Standard Schema validator
59
* @param validator - Validator to check
60
* @returns True if validator implements Standard Schema
61
*/
62
function isStandardSchemaValidator(
63
validator: unknown,
64
): validator is StandardSchemaV1;
65
```
66
67
### Standard Schema Validators
68
69
Helper object for working with Standard Schema validators.
70
71
```typescript { .api }
72
/**
73
* Standard Schema validation helpers
74
*/
75
const standardSchemaValidators: {
76
/**
77
* Synchronously validates a value using a Standard Schema validator
78
* @param value - Value to validate
79
* @param schema - Standard Schema validator
80
* @returns Validation error or undefined if valid
81
*/
82
validate<TInput, TOutput>(
83
value: TInput,
84
schema: StandardSchemaV1<TInput, TOutput>,
85
): ValidationError | undefined;
86
87
/**
88
* Asynchronously validates a value using a Standard Schema validator
89
* @param value - Value to validate
90
* @param schema - Standard Schema validator
91
* @returns Promise resolving to validation error or undefined if valid
92
*/
93
validateAsync<TInput, TOutput>(
94
value: TInput,
95
schema: StandardSchemaV1<TInput, TOutput>,
96
): Promise<ValidationError | undefined>;
97
};
98
```
99
100
### Validation Logic Functions
101
102
Control when and how validation runs with custom logic functions.
103
104
```typescript { .api }
105
/**
106
* Default validation logic that runs validators based on event type
107
* Runs onMount, onChange, onBlur, onSubmit validators at their respective triggers
108
*/
109
const defaultValidationLogic: ValidationLogicFn;
110
111
/**
112
* Validation logic similar to React Hook Form
113
* Only validates dynamically after first submission attempt
114
*
115
* @param props.mode - Validation mode before submission ('change' | 'blur' | 'submit')
116
* @param props.modeAfterSubmission - Validation mode after submission ('change' | 'blur' | 'submit')
117
* @returns Validation logic function
118
*/
119
function revalidateLogic(props?: {
120
mode?: 'change' | 'blur' | 'submit';
121
modeAfterSubmission?: 'change' | 'blur' | 'submit';
122
}): ValidationLogicFn;
123
124
/**
125
* Validation logic function type
126
* Determines validation behavior based on form state and trigger cause
127
* @param props.cause - Reason for validation
128
* @param props.validator - Validator type to check
129
* @param props.value - Current value
130
* @param props.formApi - Form API instance
131
*/
132
type ValidationLogicFn = (props: ValidationLogicProps) => void;
133
134
interface ValidationLogicProps {
135
/** Reason for validation trigger */
136
cause: ValidationCause;
137
138
/** Type of validator being checked */
139
validator: ValidationErrorMapKeys;
140
141
/** Current form or field value */
142
value: unknown;
143
144
/** Form API instance */
145
formApi: AnyFormApi;
146
}
147
```
148
149
### Validation Types
150
151
Core types for validation errors and triggers.
152
153
```typescript { .api }
154
/** Validation error - can be any type (string, object, etc.) */
155
type ValidationError = unknown;
156
157
/** Validation trigger cause */
158
type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | 'server' | 'dynamic';
159
160
/** Listener trigger cause (subset of ValidationCause) */
161
type ListenerCause = 'change' | 'blur' | 'submit' | 'mount';
162
163
/** Validation source (form-level or field-level) */
164
type ValidationSource = 'form' | 'field';
165
166
/** Keys for validation error maps (e.g., 'onMount', 'onChange', 'onBlur') */
167
type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`;
168
169
/**
170
* Map of validation errors by trigger type
171
*/
172
type ValidationErrorMap = Partial<Record<ValidationErrorMapKeys, ValidationError>>;
173
174
/**
175
* Map of validation error sources by trigger type
176
*/
177
type ValidationErrorMapSource = Partial<Record<ValidationErrorMapKeys, ValidationSource>>;
178
179
/**
180
* Form-level validation error map
181
* Extends ValidationErrorMap to support global form errors
182
*/
183
type FormValidationErrorMap<
184
TFormData,
185
TOnMount extends undefined | FormValidateOrFn<TFormData>,
186
TOnChange extends undefined | FormValidateOrFn<TFormData>,
187
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
188
TOnBlur extends undefined | FormValidateOrFn<TFormData>,
189
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
190
TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
191
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
192
TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
193
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
194
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
195
> = {
196
onMount?: UnwrapFormValidateOrFn<TOnMount>;
197
onChange?: UnwrapFormValidateOrFn<TOnChange>;
198
onChangeAsync?: UnwrapFormAsyncValidateOrFn<TOnChangeAsync>;
199
onBlur?: UnwrapFormValidateOrFn<TOnBlur>;
200
onBlurAsync?: UnwrapFormAsyncValidateOrFn<TOnBlurAsync>;
201
onSubmit?: UnwrapFormValidateOrFn<TOnSubmit>;
202
onSubmitAsync?: UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>;
203
onDynamic?: UnwrapFormValidateOrFn<TOnDynamic>;
204
onDynamicAsync?: UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>;
205
onServer?: UnwrapFormAsyncValidateOrFn<TOnServer>;
206
};
207
```
208
209
### Global Form Validation Errors
210
211
Support for validation errors that affect both form-level and field-level state.
212
213
```typescript { .api }
214
/**
215
* Global form validation error
216
* Can specify both form-level error and field-specific errors
217
*/
218
interface GlobalFormValidationError<TFormData> {
219
/** Form-level error message */
220
form?: ValidationError;
221
222
/** Map of field-specific errors by field path */
223
fields: Partial<Record<DeepKeys<TFormData>, ValidationError>>;
224
}
225
226
/**
227
* Form validation error type
228
* Can be either a simple error or a global error with field mapping
229
*/
230
type FormValidationError<TFormData> =
231
| ValidationError
232
| GlobalFormValidationError<TFormData>;
233
234
/**
235
* Type guard to check if error is a global form validation error
236
* @param error - Error to check
237
* @returns True if error is GlobalFormValidationError
238
*/
239
function isGlobalFormValidationError(
240
error: unknown,
241
): error is GlobalFormValidationError<any>;
242
243
/** Extracts the form-level error from a GlobalFormValidationError */
244
type ExtractGlobalFormError<T> = T extends GlobalFormValidationError<any>
245
? T['form']
246
: T;
247
```
248
249
### Validator Helper Types
250
251
Types for unwrapping validator return values.
252
253
```typescript { .api }
254
/** Unwraps return type from form validator */
255
type UnwrapFormValidateOrFn<T> = T extends FormValidateFn<any>
256
? ReturnType<T> extends Promise<infer U>
257
? U
258
: ReturnType<T>
259
: T extends StandardSchemaV1<any, any>
260
? ValidationError
261
: undefined;
262
263
/** Unwraps return type from async form validator */
264
type UnwrapFormAsyncValidateOrFn<T> = T extends FormValidateAsyncFn<any>
265
? Awaited<ReturnType<T>>
266
: T extends StandardSchemaV1<any, any>
267
? ValidationError
268
: undefined;
269
270
/** Unwraps return type from field validator */
271
type UnwrapFieldValidateOrFn<T> = T extends FieldValidateFn<any, any, any>
272
? ReturnType<T> extends Promise<infer U>
273
? U
274
: ReturnType<T>
275
: T extends StandardSchemaV1<any, any>
276
? ValidationError
277
: undefined;
278
279
/** Unwraps return type from async field validator */
280
type UnwrapFieldAsyncValidateOrFn<T> = T extends FieldValidateAsyncFn<any, any, any>
281
? Awaited<ReturnType<T>>
282
: T extends StandardSchemaV1<any, any>
283
? ValidationError
284
: undefined;
285
```
286
287
### Validator Interface Types
288
289
```typescript { .api }
290
/** Asynchronous validator interface */
291
interface AsyncValidator<TInput> {
292
(value: TInput, signal: AbortSignal): Promise<ValidationError | undefined>;
293
}
294
295
/** Synchronous validator interface */
296
interface SyncValidator<TInput> {
297
(value: TInput): ValidationError | undefined;
298
}
299
```
300
301
## Usage Examples
302
303
### Using Zod Schema Validation
304
305
```typescript
306
import { useForm } from '@tanstack/react-form';
307
import { z } from 'zod';
308
309
const userSchema = z.object({
310
name: z.string().min(2, 'Name must be at least 2 characters'),
311
email: z.string().email('Invalid email address'),
312
age: z.number().min(18, 'Must be at least 18 years old'),
313
});
314
315
function UserForm() {
316
const form = useForm({
317
defaultValues: {
318
name: '',
319
email: '',
320
age: 0,
321
},
322
validators: {
323
onChange: userSchema,
324
},
325
});
326
327
return (
328
<form onSubmit={(e) => {
329
e.preventDefault();
330
form.handleSubmit();
331
}}>
332
<form.Field name="email" validators={{ onChange: z.string().email() }}>
333
{(field) => (
334
<div>
335
<input
336
value={field.state.value}
337
onChange={(e) => field.handleChange(e.target.value)}
338
/>
339
{field.state.meta.errors[0] && (
340
<span>{field.state.meta.errors[0]}</span>
341
)}
342
</div>
343
)}
344
</form.Field>
345
</form>
346
);
347
}
348
```
349
350
### Custom Validation Functions
351
352
```typescript
353
import { useForm } from '@tanstack/react-form';
354
355
function PasswordForm() {
356
const form = useForm({
357
defaultValues: {
358
password: '',
359
confirmPassword: '',
360
},
361
validators: {
362
onChange: ({ value }) => {
363
if (value.password !== value.confirmPassword) {
364
return {
365
form: 'Passwords do not match',
366
fields: {
367
confirmPassword: 'Must match password',
368
},
369
};
370
}
371
return undefined;
372
},
373
},
374
});
375
376
return (
377
<form.Field
378
name="password"
379
validators={{
380
onChange: ({ value }) => {
381
if (value.length < 8) {
382
return 'Password must be at least 8 characters';
383
}
384
if (!/[A-Z]/.test(value)) {
385
return 'Password must contain an uppercase letter';
386
}
387
if (!/[0-9]/.test(value)) {
388
return 'Password must contain a number';
389
}
390
return undefined;
391
},
392
}}
393
>
394
{(field) => (
395
<div>
396
<input
397
type="password"
398
value={field.state.value}
399
onChange={(e) => field.handleChange(e.target.value)}
400
/>
401
{field.state.meta.errors[0]}
402
</div>
403
)}
404
</form.Field>
405
);
406
}
407
```
408
409
### Async Validation with Debouncing
410
411
```typescript
412
function UsernameField() {
413
const form = useForm({
414
defaultValues: {
415
username: '',
416
},
417
asyncDebounceMs: 500,
418
});
419
420
return (
421
<form.Field
422
name="username"
423
validators={{
424
onChange: ({ value }) => {
425
if (value.length < 3) {
426
return 'Username must be at least 3 characters';
427
}
428
return undefined;
429
},
430
onChangeAsync: async ({ value, signal }) => {
431
try {
432
const response = await fetch(
433
`/api/check-username?username=${value}`,
434
{ signal }
435
);
436
const data = await response.json();
437
return data.available ? undefined : 'Username already taken';
438
} catch (error) {
439
if (error.name === 'AbortError') {
440
return undefined; // Validation was cancelled
441
}
442
throw error;
443
}
444
},
445
}}
446
>
447
{(field) => (
448
<div>
449
<input
450
value={field.state.value}
451
onChange={(e) => field.handleChange(e.target.value)}
452
/>
453
{field.state.meta.isValidating && <span>Checking...</span>}
454
{field.state.meta.errors[0] && (
455
<span>{field.state.meta.errors[0]}</span>
456
)}
457
</div>
458
)}
459
</form.Field>
460
);
461
}
462
```
463
464
### Custom Validation Logic
465
466
```typescript
467
import { useForm, revalidateLogic } from '@tanstack/react-form';
468
469
function ReactHookFormStyleValidation() {
470
const form = useForm({
471
defaultValues: {
472
email: '',
473
},
474
// Only validate on blur before submission, on change after submission
475
validationLogic: revalidateLogic({
476
mode: 'blur',
477
modeAfterSubmission: 'change',
478
}),
479
validators: {
480
onBlur: ({ value }) => {
481
if (!value.email.includes('@')) {
482
return { form: 'Invalid email' };
483
}
484
return undefined;
485
},
486
onChange: ({ value }) => {
487
if (!value.email.includes('@')) {
488
return { form: 'Invalid email' };
489
}
490
return undefined;
491
},
492
},
493
});
494
495
return <form>{/* fields */}</form>;
496
}
497
```
498
499
### Multiple Validators Per Event
500
501
```typescript
502
function MultiValidatorField() {
503
const form = useForm({
504
defaultValues: {
505
email: '',
506
},
507
});
508
509
return (
510
<form.Field
511
name="email"
512
validators={{
513
onChange: [
514
// Array of validators - all must pass
515
({ value }) => (!value ? 'Email is required' : undefined),
516
({ value }) => (!value.includes('@') ? 'Must include @' : undefined),
517
({ value }) => (!value.includes('.') ? 'Must include domain' : undefined),
518
],
519
}}
520
>
521
{(field) => (
522
<div>
523
<input
524
value={field.state.value}
525
onChange={(e) => field.handleChange(e.target.value)}
526
/>
527
{field.state.meta.errors.map((error, i) => (
528
<div key={i}>{String(error)}</div>
529
))}
530
</div>
531
)}
532
</form.Field>
533
);
534
}
535
```
536
537
### Conditional Field Validation
538
539
```typescript
540
function ConditionalValidation() {
541
const form = useForm({
542
defaultValues: {
543
shippingMethod: 'standard',
544
trackingNumber: '',
545
},
546
});
547
548
return (
549
<>
550
<form.Field name="shippingMethod">
551
{(field) => (
552
<select
553
value={field.state.value}
554
onChange={(e) => field.handleChange(e.target.value)}
555
>
556
<option value="standard">Standard</option>
557
<option value="express">Express</option>
558
</select>
559
)}
560
</form.Field>
561
562
<form.Field
563
name="trackingNumber"
564
validators={{
565
onChangeListenTo: ['shippingMethod'],
566
onChange: ({ value, fieldApi }) => {
567
const shippingMethod = fieldApi.form.getFieldValue('shippingMethod');
568
if (shippingMethod === 'express' && !value) {
569
return 'Tracking number required for express shipping';
570
}
571
return undefined;
572
},
573
}}
574
>
575
{(field) => (
576
<div>
577
<input
578
value={field.state.value}
579
onChange={(e) => field.handleChange(e.target.value)}
580
/>
581
{field.state.meta.errors[0]}
582
</div>
583
)}
584
</form.Field>
585
</>
586
);
587
}
588
```
589
590
### Global Form Validation with Field Errors
591
592
```typescript
593
function OrderForm() {
594
const form = useForm({
595
defaultValues: {
596
items: [],
597
total: 0,
598
},
599
validators: {
600
onSubmit: ({ value }) => {
601
if (value.items.length === 0) {
602
return {
603
form: 'Order must have at least one item',
604
fields: {
605
items: 'Add at least one item to the order',
606
},
607
};
608
}
609
610
const calculatedTotal = value.items.reduce(
611
(sum, item) => sum + item.price,
612
0
613
);
614
615
if (calculatedTotal !== value.total) {
616
return {
617
form: 'Total does not match items',
618
fields: {
619
total: `Expected ${calculatedTotal}, got ${value.total}`,
620
},
621
};
622
}
623
624
return undefined;
625
},
626
},
627
});
628
629
return (
630
<form onSubmit={(e) => {
631
e.preventDefault();
632
form.handleSubmit();
633
}}>
634
{form.state.errors.length > 0 && (
635
<div className="form-error">
636
{form.state.errors.map((error, i) => (
637
<div key={i}>{String(error)}</div>
638
))}
639
</div>
640
)}
641
{/* fields */}
642
</form>
643
);
644
}
645
```
646