CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--react-form

Powerful, type-safe forms for React.

Overview
Eval results
Files

validation.mddocs/

Validation System

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.

Capabilities

Standard Schema Integration

TanStack React Form implements the Standard Schema specification for validation library integration.

/**
 * Standard Schema validator interface (v1)
 * Implemented by validation libraries like Zod, Yup, Valibot, Joi, etc.
 */
interface StandardSchemaV1<Input = unknown, Output = Input> {
  readonly '~standard': StandardSchemaV1.Props<Input, Output>;
}

namespace StandardSchemaV1 {
  interface Props<Input, Output> {
    /** Standard Schema version */
    readonly version: 1;

    /** Library vendor identifier */
    readonly vendor: string;

    /**
     * Validation function
     * @param value - Value to validate
     * @returns Validation result with output value or issues
     */
    readonly validate: (
      value: unknown,
    ) => Result<Output> | Promise<Result<Output>>;
  }

  interface Result<Output> {
    /** Validated and transformed output value (present if valid) */
    readonly value?: Output;

    /** Validation issues/errors (present if invalid) */
    readonly issues?: ReadonlyArray<StandardSchemaV1Issue>;
  }
}

/**
 * Standard Schema issue/error interface
 */
interface StandardSchemaV1Issue {
  /** Human-readable error message */
  readonly message: string;

  /** Path to the field with error (e.g., 'user.email') */
  readonly path?: ReadonlyArray<string | number | symbol>;
}

/**
 * Type guard to check if a validator is a Standard Schema validator
 * @param validator - Validator to check
 * @returns True if validator implements Standard Schema
 */
function isStandardSchemaValidator(
  validator: unknown,
): validator is StandardSchemaV1;

Standard Schema Validators

Helper object for working with Standard Schema validators.

/**
 * Standard Schema validation helpers
 */
const standardSchemaValidators: {
  /**
   * Synchronously validates a value using a Standard Schema validator
   * @param value - Value to validate
   * @param schema - Standard Schema validator
   * @returns Validation error or undefined if valid
   */
  validate<TInput, TOutput>(
    value: TInput,
    schema: StandardSchemaV1<TInput, TOutput>,
  ): ValidationError | undefined;

  /**
   * Asynchronously validates a value using a Standard Schema validator
   * @param value - Value to validate
   * @param schema - Standard Schema validator
   * @returns Promise resolving to validation error or undefined if valid
   */
  validateAsync<TInput, TOutput>(
    value: TInput,
    schema: StandardSchemaV1<TInput, TOutput>,
  ): Promise<ValidationError | undefined>;
};

Validation Logic Functions

Control when and how validation runs with custom logic functions.

/**
 * Default validation logic that runs validators based on event type
 * Runs onMount, onChange, onBlur, onSubmit validators at their respective triggers
 */
const defaultValidationLogic: ValidationLogicFn;

/**
 * Validation logic similar to React Hook Form
 * Only validates dynamically after first submission attempt
 *
 * @param props.mode - Validation mode before submission ('change' | 'blur' | 'submit')
 * @param props.modeAfterSubmission - Validation mode after submission ('change' | 'blur' | 'submit')
 * @returns Validation logic function
 */
function revalidateLogic(props?: {
  mode?: 'change' | 'blur' | 'submit';
  modeAfterSubmission?: 'change' | 'blur' | 'submit';
}): ValidationLogicFn;

/**
 * Validation logic function type
 * Determines validation behavior based on form state and trigger cause
 * @param props.cause - Reason for validation
 * @param props.validator - Validator type to check
 * @param props.value - Current value
 * @param props.formApi - Form API instance
 */
type ValidationLogicFn = (props: ValidationLogicProps) => void;

interface ValidationLogicProps {
  /** Reason for validation trigger */
  cause: ValidationCause;

  /** Type of validator being checked */
  validator: ValidationErrorMapKeys;

  /** Current form or field value */
  value: unknown;

  /** Form API instance */
  formApi: AnyFormApi;
}

Validation Types

Core types for validation errors and triggers.

/** Validation error - can be any type (string, object, etc.) */
type ValidationError = unknown;

/** Validation trigger cause */
type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | 'server' | 'dynamic';

/** Listener trigger cause (subset of ValidationCause) */
type ListenerCause = 'change' | 'blur' | 'submit' | 'mount';

/** Validation source (form-level or field-level) */
type ValidationSource = 'form' | 'field';

/** Keys for validation error maps (e.g., 'onMount', 'onChange', 'onBlur') */
type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`;

/**
 * Map of validation errors by trigger type
 */
type ValidationErrorMap = Partial<Record<ValidationErrorMapKeys, ValidationError>>;

/**
 * Map of validation error sources by trigger type
 */
type ValidationErrorMapSource = Partial<Record<ValidationErrorMapKeys, ValidationSource>>;

/**
 * Form-level validation error map
 * Extends ValidationErrorMap to support global form errors
 */
type FormValidationErrorMap<
  TFormData,
  TOnMount extends undefined | FormValidateOrFn<TFormData>,
  TOnChange extends undefined | FormValidateOrFn<TFormData>,
  TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnBlur extends undefined | FormValidateOrFn<TFormData>,
  TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
  TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
  TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> = {
  onMount?: UnwrapFormValidateOrFn<TOnMount>;
  onChange?: UnwrapFormValidateOrFn<TOnChange>;
  onChangeAsync?: UnwrapFormAsyncValidateOrFn<TOnChangeAsync>;
  onBlur?: UnwrapFormValidateOrFn<TOnBlur>;
  onBlurAsync?: UnwrapFormAsyncValidateOrFn<TOnBlurAsync>;
  onSubmit?: UnwrapFormValidateOrFn<TOnSubmit>;
  onSubmitAsync?: UnwrapFormAsyncValidateOrFn<TOnSubmitAsync>;
  onDynamic?: UnwrapFormValidateOrFn<TOnDynamic>;
  onDynamicAsync?: UnwrapFormAsyncValidateOrFn<TOnDynamicAsync>;
  onServer?: UnwrapFormAsyncValidateOrFn<TOnServer>;
};

Global Form Validation Errors

Support for validation errors that affect both form-level and field-level state.

/**
 * Global form validation error
 * Can specify both form-level error and field-specific errors
 */
interface GlobalFormValidationError<TFormData> {
  /** Form-level error message */
  form?: ValidationError;

  /** Map of field-specific errors by field path */
  fields: Partial<Record<DeepKeys<TFormData>, ValidationError>>;
}

/**
 * Form validation error type
 * Can be either a simple error or a global error with field mapping
 */
type FormValidationError<TFormData> =
  | ValidationError
  | GlobalFormValidationError<TFormData>;

/**
 * Type guard to check if error is a global form validation error
 * @param error - Error to check
 * @returns True if error is GlobalFormValidationError
 */
function isGlobalFormValidationError(
  error: unknown,
): error is GlobalFormValidationError<any>;

/** Extracts the form-level error from a GlobalFormValidationError */
type ExtractGlobalFormError<T> = T extends GlobalFormValidationError<any>
  ? T['form']
  : T;

Validator Helper Types

Types for unwrapping validator return values.

/** Unwraps return type from form validator */
type UnwrapFormValidateOrFn<T> = T extends FormValidateFn<any>
  ? ReturnType<T> extends Promise<infer U>
    ? U
    : ReturnType<T>
  : T extends StandardSchemaV1<any, any>
    ? ValidationError
    : undefined;

/** Unwraps return type from async form validator */
type UnwrapFormAsyncValidateOrFn<T> = T extends FormValidateAsyncFn<any>
  ? Awaited<ReturnType<T>>
  : T extends StandardSchemaV1<any, any>
    ? ValidationError
    : undefined;

/** Unwraps return type from field validator */
type UnwrapFieldValidateOrFn<T> = T extends FieldValidateFn<any, any, any>
  ? ReturnType<T> extends Promise<infer U>
    ? U
    : ReturnType<T>
  : T extends StandardSchemaV1<any, any>
    ? ValidationError
    : undefined;

/** Unwraps return type from async field validator */
type UnwrapFieldAsyncValidateOrFn<T> = T extends FieldValidateAsyncFn<any, any, any>
  ? Awaited<ReturnType<T>>
  : T extends StandardSchemaV1<any, any>
    ? ValidationError
    : undefined;

Validator Interface Types

/** Asynchronous validator interface */
interface AsyncValidator<TInput> {
  (value: TInput, signal: AbortSignal): Promise<ValidationError | undefined>;
}

/** Synchronous validator interface */
interface SyncValidator<TInput> {
  (value: TInput): ValidationError | undefined;
}

Usage Examples

Using Zod Schema Validation

import { useForm } from '@tanstack/react-form';
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  age: z.number().min(18, 'Must be at least 18 years old'),
});

function UserForm() {
  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
      age: 0,
    },
    validators: {
      onChange: userSchema,
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      form.handleSubmit();
    }}>
      <form.Field name="email" validators={{ onChange: z.string().email() }}>
        {(field) => (
          <div>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors[0] && (
              <span>{field.state.meta.errors[0]}</span>
            )}
          </div>
        )}
      </form.Field>
    </form>
  );
}

Custom Validation Functions

import { useForm } from '@tanstack/react-form';

function PasswordForm() {
  const form = useForm({
    defaultValues: {
      password: '',
      confirmPassword: '',
    },
    validators: {
      onChange: ({ value }) => {
        if (value.password !== value.confirmPassword) {
          return {
            form: 'Passwords do not match',
            fields: {
              confirmPassword: 'Must match password',
            },
          };
        }
        return undefined;
      },
    },
  });

  return (
    <form.Field
      name="password"
      validators={{
        onChange: ({ value }) => {
          if (value.length < 8) {
            return 'Password must be at least 8 characters';
          }
          if (!/[A-Z]/.test(value)) {
            return 'Password must contain an uppercase letter';
          }
          if (!/[0-9]/.test(value)) {
            return 'Password must contain a number';
          }
          return undefined;
        },
      }}
    >
      {(field) => (
        <div>
          <input
            type="password"
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
          />
          {field.state.meta.errors[0]}
        </div>
      )}
    </form.Field>
  );
}

Async Validation with Debouncing

function UsernameField() {
  const form = useForm({
    defaultValues: {
      username: '',
    },
    asyncDebounceMs: 500,
  });

  return (
    <form.Field
      name="username"
      validators={{
        onChange: ({ value }) => {
          if (value.length < 3) {
            return 'Username must be at least 3 characters';
          }
          return undefined;
        },
        onChangeAsync: async ({ value, signal }) => {
          try {
            const response = await fetch(
              `/api/check-username?username=${value}`,
              { signal }
            );
            const data = await response.json();
            return data.available ? undefined : 'Username already taken';
          } catch (error) {
            if (error.name === 'AbortError') {
              return undefined; // Validation was cancelled
            }
            throw error;
          }
        },
      }}
    >
      {(field) => (
        <div>
          <input
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
          />
          {field.state.meta.isValidating && <span>Checking...</span>}
          {field.state.meta.errors[0] && (
            <span>{field.state.meta.errors[0]}</span>
          )}
        </div>
      )}
    </form.Field>
  );
}

Custom Validation Logic

import { useForm, revalidateLogic } from '@tanstack/react-form';

function ReactHookFormStyleValidation() {
  const form = useForm({
    defaultValues: {
      email: '',
    },
    // Only validate on blur before submission, on change after submission
    validationLogic: revalidateLogic({
      mode: 'blur',
      modeAfterSubmission: 'change',
    }),
    validators: {
      onBlur: ({ value }) => {
        if (!value.email.includes('@')) {
          return { form: 'Invalid email' };
        }
        return undefined;
      },
      onChange: ({ value }) => {
        if (!value.email.includes('@')) {
          return { form: 'Invalid email' };
        }
        return undefined;
      },
    },
  });

  return <form>{/* fields */}</form>;
}

Multiple Validators Per Event

function MultiValidatorField() {
  const form = useForm({
    defaultValues: {
      email: '',
    },
  });

  return (
    <form.Field
      name="email"
      validators={{
        onChange: [
          // Array of validators - all must pass
          ({ value }) => (!value ? 'Email is required' : undefined),
          ({ value }) => (!value.includes('@') ? 'Must include @' : undefined),
          ({ value }) => (!value.includes('.') ? 'Must include domain' : undefined),
        ],
      }}
    >
      {(field) => (
        <div>
          <input
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
          />
          {field.state.meta.errors.map((error, i) => (
            <div key={i}>{String(error)}</div>
          ))}
        </div>
      )}
    </form.Field>
  );
}

Conditional Field Validation

function ConditionalValidation() {
  const form = useForm({
    defaultValues: {
      shippingMethod: 'standard',
      trackingNumber: '',
    },
  });

  return (
    <>
      <form.Field name="shippingMethod">
        {(field) => (
          <select
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
          >
            <option value="standard">Standard</option>
            <option value="express">Express</option>
          </select>
        )}
      </form.Field>

      <form.Field
        name="trackingNumber"
        validators={{
          onChangeListenTo: ['shippingMethod'],
          onChange: ({ value, fieldApi }) => {
            const shippingMethod = fieldApi.form.getFieldValue('shippingMethod');
            if (shippingMethod === 'express' && !value) {
              return 'Tracking number required for express shipping';
            }
            return undefined;
          },
        }}
      >
        {(field) => (
          <div>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors[0]}
          </div>
        )}
      </form.Field>
    </>
  );
}

Global Form Validation with Field Errors

function OrderForm() {
  const form = useForm({
    defaultValues: {
      items: [],
      total: 0,
    },
    validators: {
      onSubmit: ({ value }) => {
        if (value.items.length === 0) {
          return {
            form: 'Order must have at least one item',
            fields: {
              items: 'Add at least one item to the order',
            },
          };
        }

        const calculatedTotal = value.items.reduce(
          (sum, item) => sum + item.price,
          0
        );

        if (calculatedTotal !== value.total) {
          return {
            form: 'Total does not match items',
            fields: {
              total: `Expected ${calculatedTotal}, got ${value.total}`,
            },
          };
        }

        return undefined;
      },
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      form.handleSubmit();
    }}>
      {form.state.errors.length > 0 && (
        <div className="form-error">
          {form.state.errors.map((error, i) => (
            <div key={i}>{String(error)}</div>
          ))}
        </div>
      )}
      {/* fields */}
    </form>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-tanstack--react-form

docs

advanced.md

field-api.md

form-api.md

framework-integrations.md

hooks.md

index.md

validation.md

tile.json