CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tanstack--react-form

Powerful, type-safe forms for React.

Overview
Eval results
Files

framework-integrations.mddocs/

Framework Integrations

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.

Capabilities

Next.js Integration

Server-side validation for Next.js Server Actions.

/**
 * Creates a server validation function for Next.js Server Actions
 * Validates FormData on the server and throws ServerValidateError on failure
 *
 * @param defaultOpts - Configuration for server validation
 * @returns Async function that validates FormData and returns parsed data
 */
function createServerValidate<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
>(
  defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,
): (
  formData: FormData,
  info?: { resolve?: (fieldName: string) => string | File },
) => Promise<TFormData>;

interface CreateServerValidateOptions<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> {
  /** Default form values */
  defaultValues?: TFormData;

  /**
   * Server-side validator function or Standard Schema
   * Validates the parsed form data before processing
   */
  onServerValidate?: TOnServer;

  /**
   * Custom parser for FormData
   * @param formData - Raw FormData from form submission
   * @returns Parsed form data object
   */
  parse?: (formData: FormData) => TFormData;
}

/**
 * Initial form state constant
 * Use this as default state before server validation completes
 */
const initialFormState: ServerFormState<any, undefined>;

/**
 * Server validation error class
 * Thrown by createServerValidate when validation fails
 */
class ServerValidateError<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> extends Error {
  /** Form state with validation errors */
  formState: ServerFormState<TFormData, TOnServer>;

  constructor(formState: ServerFormState<TFormData, TOnServer>);
}

/**
 * Server form state type
 * Subset of full FormState used for server-side validation
 */
type ServerFormState<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> = Pick<
  FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,
  'values' | 'errors' | 'errorMap'
>;

Usage Example:

// app/actions.ts (Next.js Server Action)
'use server';

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

const serverValidate = createServerValidate({
  defaultValues: {
    name: '',
    email: '',
  },
  onServerValidate: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
});

export async function submitForm(prev: any, formData: FormData) {
  try {
    const data = await serverValidate(formData);

    // Process validated data
    await saveToDatabase(data);

    return { success: true };
  } catch (error) {
    if (error instanceof ServerValidateError) {
      return error.formState;
    }
    throw error;
  }
}

// app/form.tsx (Client Component)
'use client';

import { useForm } from '@tanstack/react-form';
import { useFormState } from 'react-dom';
import { submitForm } from './actions';
import { initialFormState } from '@tanstack/react-form/nextjs';

export function MyForm() {
  const [serverFormState, formAction] = useFormState(submitForm, initialFormState);

  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
    },
    // Merge server validation errors with client state
    defaultState: serverFormState,
  });

  return (
    <form action={formAction}>
      <form.Field name="name">
        {(field) => (
          <input
            name={field.name}
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
          />
        )}
      </form.Field>

      <form.Field name="email">
        {(field) => (
          <input
            name={field.name}
            value={field.state.value}
            onChange={(e) => field.handleChange(e.target.value)}
          />
        )}
      </form.Field>

      <button type="submit">Submit</button>
    </form>
  );
}

Remix Integration

Server-side validation for Remix Form Actions.

/**
 * Creates a server validation function for Remix actions
 * Validates FormData on the server and throws ServerValidateError on failure
 *
 * @param defaultOpts - Configuration for server validation
 * @returns Async function that validates FormData and returns parsed data
 */
function createServerValidate<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
>(
  defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,
): (
  formData: FormData,
  info?: { resolve?: (fieldName: string) => string | File },
) => Promise<TFormData>;

/**
 * Initial form state constant
 * Use this as default state in loader functions
 */
const initialFormState: ServerFormState<any, undefined>;

/**
 * Server validation error class
 * Thrown by createServerValidate when validation fails
 */
class ServerValidateError<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> extends Error {
  /** Form state with validation errors */
  formState: ServerFormState<TFormData, TOnServer>;

  constructor(formState: ServerFormState<TFormData, TOnServer>);
}

/** Server form state type (same as Next.js) */
type ServerFormState<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> = Pick<
  FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,
  'values' | 'errors' | 'errorMap'
>;

Usage Example:

// app/routes/contact.tsx (Remix)
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { useActionData, Form } from '@remix-run/react';
import { useForm } from '@tanstack/react-form';
import { createServerValidate, initialFormState } from '@tanstack/react-form/remix';
import { z } from 'zod';

const serverValidate = createServerValidate({
  defaultValues: {
    name: '',
    email: '',
    message: '',
  },
  onServerValidate: z.object({
    name: z.string().min(2),
    email: z.string().email(),
    message: z.string().min(10),
  }),
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();

  try {
    const data = await serverValidate(formData);

    // Process validated data
    await sendEmail(data);

    return json({ success: true });
  } catch (error) {
    if (error instanceof ServerValidateError) {
      return json(error.formState);
    }
    throw error;
  }
}

export default function ContactRoute() {
  const actionData = useActionData<typeof action>();
  const serverFormState = actionData?.success ? initialFormState : actionData;

  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
      message: '',
    },
    defaultState: serverFormState,
  });

  return (
    <Form method="post">
      <form.Field name="name">
        {(field) => (
          <div>
            <input
              name={field.name}
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors[0]}
          </div>
        )}
      </form.Field>

      <form.Field name="email">
        {(field) => (
          <div>
            <input
              name={field.name}
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors[0]}
          </div>
        )}
      </form.Field>

      <form.Field name="message">
        {(field) => (
          <div>
            <textarea
              name={field.name}
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors[0]}
          </div>
        )}
      </form.Field>

      <button type="submit">Submit</button>
    </Form>
  );
}

TanStack Start Integration

Server-side validation for TanStack Start with cookie-based state persistence.

/**
 * Creates a server validation function for TanStack Start
 * Validates FormData and stores state in cookies for post-redirect access
 *
 * @param defaultOpts - Configuration for server validation
 * @returns Async function that validates FormData and returns parsed data
 */
function createServerValidate<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined,
>(
  defaultOpts: CreateServerValidateOptions<TFormData, TOnServer>,
): (
  formData: FormData,
  info?: { resolve?: (fieldName: string) => string | File },
) => Promise<TFormData>;

/**
 * Retrieves form data from cookies set by server validation
 * Use this in your component to access validation state after redirect
 *
 * @returns Promise resolving to server form state or initialFormState
 */
function getFormData(): Promise<
  ServerFormState<any, undefined> | typeof initialFormState
>;

/**
 * Initial form state constant
 */
const initialFormState: {
  errorMap: { onServer: undefined };
  errors: [];
};

/**
 * Server validation error class for TanStack Start
 * Includes Response object for redirect handling
 */
class ServerValidateError<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> extends Error {
  /** Form state with validation errors */
  formState: ServerFormState<TFormData, TOnServer>;

  /** Response object for redirects and headers */
  response: Response;

  constructor(formState: ServerFormState<TFormData, TOnServer>, response: Response);
}

/** Server form state type */
type ServerFormState<
  TFormData,
  TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
> = Pick<
  FormState<TFormData, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, TOnServer>,
  'values' | 'errors' | 'errorMap'
>;

Usage Example:

// app/routes/contact.tsx (TanStack Start)
import { createServerFn } from '@tanstack/start';
import { useServerFn } from '@tanstack/start';
import { useForm } from '@tanstack/react-form';
import {
  createServerValidate,
  getFormData,
  initialFormState,
  ServerValidateError,
} from '@tanstack/react-form/start';
import { z } from 'zod';

const serverValidate = createServerValidate({
  defaultValues: {
    name: '',
    email: '',
  },
  onServerValidate: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
});

const submitFormAction = createServerFn({ method: 'POST' })
  .validator((formData: FormData) => formData)
  .handler(async ({ data }) => {
    try {
      const validated = await serverValidate(data);

      // Process validated data
      await saveToDatabase(validated);

      return { success: true };
    } catch (error) {
      if (error instanceof ServerValidateError) {
        // Throw the response with cookies
        throw error.response;
      }
      throw error;
    }
  });

export default function ContactRoute() {
  const serverFormState = getFormData();
  const submitForm = useServerFn(submitFormAction);

  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
    },
    defaultState: serverFormState,
    onSubmit: async ({ value }) => {
      const formData = new FormData();
      formData.append('name', value.name);
      formData.append('email', value.email);

      await submitForm({ data: formData });
    },
  });

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

      <form.Field name="email">
        {(field) => (
          <div>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors[0]}
          </div>
        )}
      </form.Field>

      <button type="submit">Submit</button>
    </form>
  );
}

Import Paths

// Next.js
import {
  createServerValidate,
  initialFormState,
  ServerValidateError,
  type ServerFormState,
} from '@tanstack/react-form/nextjs';

// Remix
import {
  createServerValidate,
  initialFormState,
  ServerValidateError,
  type ServerFormState,
} from '@tanstack/react-form/remix';

// TanStack Start
import {
  createServerValidate,
  getFormData,
  initialFormState,
  ServerValidateError,
  type ServerFormState,
} from '@tanstack/react-form/start';

Common Patterns

Progressive Enhancement

All framework integrations support Progressive Enhancement where forms work without JavaScript:

// The form submits to server even without JS
<form action={formAction} method="post">
  {/* name attribute required for server-side parsing */}
  <input name="email" />
  <button type="submit">Submit</button>
</form>

// With JS, client-side validation enhances UX
<form
  action={formAction}
  onSubmit={(e) => {
    e.preventDefault();
    form.handleSubmit();
  }}
>
  <form.Field name="email">
    {(field) => (
      <input
        name={field.name}
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
      />
    )}
  </form.Field>
</form>

Custom FormData Parsing

const serverValidate = createServerValidate({
  defaultValues: {
    tags: [],
    metadata: {},
  },
  parse: (formData) => {
    return {
      tags: formData.getAll('tags'),
      metadata: JSON.parse(formData.get('metadata') as string),
    };
  },
  onServerValidate: (data) => {
    if (data.tags.length === 0) {
      return 'At least one tag required';
    }
    return undefined;
  },
});

File Upload Handling

const serverValidate = createServerValidate({
  defaultValues: {
    file: null as File | null,
  },
  parse: (formData) => ({
    file: formData.get('file') as File,
  }),
  onServerValidate: async ({ value }) => {
    if (!value.file) {
      return { fields: { file: 'File is required' } };
    }

    if (value.file.size > 5 * 1024 * 1024) {
      return { fields: { file: 'File must be less than 5MB' } };
    }

    return undefined;
  },
});

Shared Validation Between Client and Server

// shared/validation.ts
import { z } from 'zod';

export const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Invalid email address'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
});

// server/action.ts
import { createServerValidate } from '@tanstack/react-form/nextjs';
import { contactSchema } from '../shared/validation';

export const serverValidate = createServerValidate({
  defaultValues: {
    name: '',
    email: '',
    message: '',
  },
  onServerValidate: contactSchema,
});

// client/form.tsx
import { useForm } from '@tanstack/react-form';
import { contactSchema } from '../shared/validation';

export function ContactForm() {
  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
      message: '',
    },
    validators: {
      onChange: contactSchema,
    },
  });

  return <form>{/* 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