TheShipStack Docs
Features

Forms

Type-safe forms with react-hook-form, Zod, and the Controller pattern.

TheShipStack uses react-hook-form + Zod for all forms.

Pattern

Every form follows the same structure: define a Zod schema, derive the TypeScript type from it, pass the schema to zodResolver, and use Controller for each field.

'use client'

import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

const schema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
})

type FormValues = z.infer<typeof schema>

export function ExampleForm() {
  const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(schema),
  })

  function onSubmit(data: FormValues) {
    // data is fully typed
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <Controller
        control={control}
        name="name"
        render={({ field }) => (
          <div>
            <Input {...field} placeholder="Name" />
            {errors.name && <p className="text-xs text-destructive">{errors.name.message}</p>}
          </div>
        )}
      />
      <Controller
        control={control}
        name="email"
        render={({ field }) => (
          <div>
            <Input {...field} placeholder="Email" type="email" />
            {errors.email && <p className="text-xs text-destructive">{errors.email.message}</p>}
          </div>
        )}
      />
      <Button type="submit">Submit</Button>
    </form>
  )
}

Conventions

  • Always use Controller — never register directly with shadcn/ui inputs
  • Define the Zod schema in the same file as the form (or a co-located schema.ts for complex forms)
  • Use z.infer<typeof schema> to derive the TypeScript type — never define it separately
  • Show field errors inline below the input, not in a toast

On this page