umma.dev

TanStack: Form

Why Should you Use TanStack for Forms?

Forms remain one of the most challenging aspects of web development. As applications grow in complexity, form management often becomes unwieldy—handling validation logic, field dependencies, error states, and maintaining performance can quickly become overwhelming. Traditional form libraries often force developers to choose between type safety, performance, or developer experience.

TanStack Form addresses these pain points with a unified solution that prioritizes type safety, performance, and developer experience simultaneously. It provides robust form management that scales with your application’s complexity without sacrificing runtime performance.

Pros of Using TanStack Form

  • Type safety throughout: From field definitions to validation and submission, everything is type-checked.
  • Framework agnostic: Works across React, Vue, Solid, and Svelte with the same core principles.
  • Excellent performance: Optimized render cycles and minimal re-renders through fine-grained updates.
  • Flexible validation: Support for synchronous and asynchronous validation with various validation libraries.
  • Developer-friendly API: Intuitive interfaces for common form operations with powerful escape hatches when needed.

What Makes TanStack Forms Different?

Field-based architecture

Unlike form libraries that treat the entire form as a single state object, TanStack Form uses a field-based architecture. Each field manages its own state independently, which results in more efficient updates and better performance. When a user types in one field, only that field re-renders—not the entire form.

Headless approach

TanStack Form provides the logic layer without dictating the UI. This separation of concerns means you have freedom over your form’s appearance and behaviour without being locked into specific UI components or styling approaches.

TypeScript support

The library leverages TypeScript’s type system to provide real-time feedback during development, catching errors before they reach production.

Framework-agnostic core

While many form libraries are tied to specific frameworks, TanStack Form separates its core logic from framework-specific implementations.

Mode-based validation

TanStack Form allows you to control when validation occurs with validation modes. You can validate on change, blur, submit, or custom triggers, giving you fine-grained control over the user experience.

Getting Started

# For React
npm install @tanstack/react-form

# For Vue
npm install @tanstack/vue-form

# For Solid
npm install @tanstack/solid-form

# For Svelte
npm install @tanstack/svelte-form
import { useForm } from "@tanstack/react-form";
import { useState } from "react";

function MyForm() {
  const form = useForm({
    defaultValues: {
      firstName: "",
      lastName: "",
      email: "",
    },
    onSubmit: async (values) => {
      // Submit values to your API
      console.log("Form submitted with values:", values);
    },
  });

  return (
    <form.Provider>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
      >
        <form.Field
          name="firstName"
          validators={{
            onChange: (value) =>
              !value ? "First name is required" : undefined,
          }}
        >
          {(field) => (
            <div>
              <label>First Name</label>
              <input
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
                onBlur={field.handleBlur}
              />
              {field.state.meta.errors ? (
                <p className="error">{field.state.meta.errors}</p>
              ) : null}
            </div>
          )}
        </form.Field>
        <button type="submit">Submit</button>
      </form>
    </form.Provider>
  );
}

Example Use Cases

Dynamic Form Fields

import { useForm, useFieldArray } from "@tanstack/react-form";
import { useState, useEffect } from "react";

function DynamicForm() {
  const form = useForm({
    defaultValues: {
      contacts: [{ name: "", email: "" }],
    },
  });

  // Create a field array to manage dynamic contacts
  const contactsArray = useFieldArray({
    name: "contacts",
    form,
  });

  return (
    <form.Provider>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          form.handleSubmit();
        }}
      >
        {contactsArray.items.map((item, index) => (
          <div key={item.id} className="contact-row">
            <form.Field name={`contacts.${index}.name`}>
              {(field) => (
                <div>
                  <label>Name</label>
                  <input
                    value={field.state.value}
                    onChange={(e) => field.handleChange(e.target.value)}
                  />
                </div>
              )}
            </form.Field>

            <form.Field name={`contacts.${index}.email`}>
              {(field) => (
                <div>
                  <label>Email</label>
                  <input
                    value={field.state.value}
                    onChange={(e) => field.handleChange(e.target.value)}
                  />
                </div>
              )}
            </form.Field>

            <button type="button" onClick={() => contactsArray.remove(index)}>
              Remove
            </button>
          </div>
        ))}

        <button
          type="button"
          onClick={() => contactsArray.append({ name: "", email: "" })}
        >
          Add Contact
        </button>

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

Complex Validation with Zod

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

// Define validation schema
const signupSchema = z
  .object({
    username: z.string().min(3, "Username must be at least 3 characters"),
    email: z.string().email("Please enter a valid email"),
    password: z.string().min(8, "Password must be at least 8 characters"),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],
  });

function SignupForm() {
  const form = useForm({
    defaultValues: {
      username: "",
      email: "",
      password: "",
      confirmPassword: "",
    },
    validatorAdapter: {
      validate: async (values) => {
        try {
          signupSchema.parse(values);
          return { success: true };
        } catch (error) {
          // Extract and format Zod errors
          const formattedErrors = {};
          if (error instanceof z.ZodError) {
            error.errors.forEach((err) => {
              formattedErrors[err.path.join(".")] = err.message;
            });
          }
          return { success: false, error: formattedErrors };
        }
      },
    },
    onSubmit: async (values) => {
      // Submit signup data
      console.log("Signup submitted with:", values);
    },
  });
}

Dependent Fields

import { useForm } from "@tanstack/react-form";
import { useState, useEffect } from "react";

function ShippingForm() {
  const form = useForm({
    defaultValues: {
      sameAsBilling: false,
      billing: {
        address: "",
        city: "",
        postcode: "",
      },
      shipping: {
        address: "",
        city: "",
        postcode: "",
      },
    },
  });

  // Get the "sameAsBilling" field state to create dependency
  const sameAsBillingField = form.useField({
    name: "sameAsBilling",
  });

  const [prevSameAsBilling, setPrevSameAsBilling] = useState(false);

  // When "sameAsBilling" changes, update shipping fields
  useEffect(() => {
    if (sameAsBillingField.state.value && !prevSameAsBilling) {
      // Copy billing values to shipping
      const billingValues = form.getFieldValue("billing");
      form.setFieldValue("shipping", billingValues);
    }
    setPrevSameAsBilling(sameAsBillingField.state.value);
  }, [sameAsBillingField.state.value]);
}

Further Reading