kb.erickguedes.com
TanStack: Ecossistema de Bibliotecas

TanStack Form: Formulários Type-Safe

Aula 4 de 5

Formulários com TanStack Form

O TanStack Form oferece gerenciamento de formulários headless com validação, arrays, campos aninhados e integração com bibliotecas de schema como Zod e Yup.

Configuração Inicial

npm install @tanstack/react-form

useForm Básico

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

function SignupForm() {
  const form = useForm({
    defaultValues: {
      name: '',
      email: '',
      age: 0,
    },
    onSubmit: async ({ value }) => {
      await fetch('/api/users', { method: 'POST', body: JSON.stringify(value) })
    },
  })

  return (
    <form onSubmit={form.handleSubmit()}>
      <form.Field
        name="name"
        validators={{
          onChange: ({ value }) =>
            value.length < 3 ? 'Nome precisa ter ao menos 3 caracteres' : undefined,
        }}
      >
        {field => (
          <div>
            <label htmlFor={field.name}>Nome</label>
            <input
              id={field.name}
              name={field.name}
              value={field.state.value}
              onChange={e => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors && (
              <p className="error">{field.state.meta.errors.join(', ')}</p>
            )}
          </div>
        )}
      </form.Field>

      <form.Field name="email">
        {field => (
          <div>
            <label htmlFor={field.name}>E-mail</label>
            <input
              id={field.name}
              value={field.state.value}
              onChange={e => field.handleChange(e.target.value)}
            />
          </div>
        )}
      </form.Field>

      <button type="submit">Cadastrar</button>
    </form>
  )
}

formOptions — Estado Compartilhado

import { formOptions } from '@tanstack/react-form'

const signupFormOpts = formOptions({
  defaultValues: { name: '', email: '', age: 0 },
  validators: {
    onSubmit: ({ value }) => {
      const parsed = signupSchema.safeParse(value)
      return parsed.success ? undefined : parsed.error.issues.map(i => i.message).join(', ')
    },
  },
})

// Em qualquer componente:
const form = useForm({ ...signupFormOpts })

Validação com Zod

import { z } from 'zod'

const userSchema = z.object({
  name: z.string().min(3, 'Mínimo 3 caracteres'),
  email: z.string().email('E-mail inválido'),
  age: z.number().min(18, 'Deve ser maior de idade'),
})

const form = useForm({
  defaultValues: { name: '', email: '', age: 0 },
  validators: {
    onSubmit: ({ value }) => {
      const result = userSchema.safeParse(value)
      return result.success ? undefined : result.error.issues[0].message
    },
  },
})

Validação Assíncrona

<form.Field
  name="email"
  validators={{
    onChangeAsync: async ({ value }) => {
      await new Promise(r => setTimeout(r, 500)) // debounce
      const taken = await checkEmailExists(value)
      return taken ? 'E-mail já cadastrado' : undefined
    },
    onChangeAsyncDebounceMs: 500,
  }}
>
  {field => <input value={field.state.value} onChange={e => field.handleChange(e.target.value)} />}
</form.Field>

Arrays e Campos Dinâmicos

function PhoneForm() {
  const form = useForm({
    defaultValues: { phones: [''] },
  })

  return (
    <form.Field name="phones" mode="array">
      {field => (
        <div>
          {field.state.value.map((_, i) => (
            <form.Field key={i} name={`phones[${i}]`}>
              {subField => (
                <div>
                  <input
                    value={subField.state.value}
                    onChange={e => subField.handleChange(e.target.value)}
                  />
                  <button type="button" onClick={() => field.removeValue(i)}>Remover</button>
                </div>
              )}
            </form.Field>
          ))}
          <button type="button" onClick={() => field.pushValue('')}>Adicionar telefone</button>
        </div>
      )}
    </form.Field>
  )
}

Custom Components

import { withFormField } from '@tanstack/react-form'

const InputField = withFormField({
  component: ({ field, label }) => (
    <div>
      <label>{label}</label>
      <input
        value={field.state.value}
        onChange={e => field.handleChange(e.target.value)}
      />
      {field.state.meta.errors && (
        <span className="error">{field.state.meta.errors[0]}</span>
      )}
    </div>
  ),
})

// Uso:
<InputField name="name" label="Nome" />

Form State

const form = useForm({ defaultValues: { name: '' } })

// Acessar estado global
const isSubmitting = form.state.isSubmitting
const errors = form.state.errors
const values = form.state.values
const canSubmit = form.state.canSubmit

// Reset
form.reset()

Lab: Formulário Multi-etapa

npm create vite@latest form-demo -- --template react-ts
cd form-demo
npm install @tanstack/react-form zod
// Construa:
// 1. Formulário de cadastro com 4+ campos (texto, email, número, select)
// 2. Validação síncrona com Zod
// 3. Um campo com validação assíncrona (ex: verificar username)
// 4. Lista dinâmica de skills (array field)
// 5. Submit com feedback visual (isSubmitting)
// 6. Botão de reset

TanStack Form torna formulários complexos previsíveis com validação declarativa, tipagem forte e suporte a cenários avançados como arrays e validação assíncrona.