TanStack

React Example: Tanstack Start

import { createFileRoute } from '@tanstack/react-router'
import { useForm } from '@tanstack/react-form'
import { useServerFn } from '@tanstack/react-start'
import { getFormDataFromServer, handleForm } from 'src/utils/form'
import { formOpts } from 'src/utils/form-isomorphic'

type HomeSearch = {
  status?: 'success'
}

export const Route = createFileRoute('/')({
  validateSearch: (search: Record<string, unknown>): HomeSearch => {
    return search.status === 'success' ? { status: 'success' } : {}
  },
  component: Home,
  loader: async () => ({
    state: await getFormDataFromServer(),
  }),
})

function Home() {
  const { state } = Route.useLoaderData()
  const { status } = Route.useSearch()
  const handleFormFn = useServerFn(handleForm)

  const form = useForm({
    ...formOpts,
    serverState: state,
  })

  return (
    <main className="page-shell">
      {status === 'success' ? (
        <p className="success-message" role="status">
          Form submitted successfully.
        </p>
      ) : null}

      <p className="eyebrow">TanStack Start</p>
      <h1 id="page-title">Server validated form</h1>
      <form
        action={handleForm.url}
        method="post"
        encType="multipart/form-data"
        noValidate
        onSubmit={async (event) => {
          event.preventDefault()
          event.stopPropagation()
          const formElement = event.currentTarget
          const errors = await form.handleSubmit()

          if (errors.length === 0) {
            await handleFormFn({ data: new FormData(formElement) })
          }
        }}
      >
        <form.Field name="firstName">
          {(field) => {
            const hasErrors = field.errors.length > 0
            const errorId = `${field.name}-errors`

            return (
              <div className="field">
                <label htmlFor={field.name}>First name</label>
                <input
                  id={field.name}
                  name={field.name}
                  type="text"
                  autoComplete="given-name"
                  value={field.value}
                  aria-invalid={hasErrors}
                  aria-describedby={hasErrors ? errorId : undefined}
                  onBlur={field.handleBlur}
                  onChange={(event) => field.handleChange(event.target.value)}
                />
                {hasErrors ? (
                  <div id={errorId} className="field-errors" role="alert">
                    {field.errors.map((error) => (
                      <p key={error.message}>{error.message}</p>
                    ))}
                  </div>
                ) : null}
              </div>
            )
          }}
        </form.Field>

        <form.Field name="age">
          {(field) => {
            const hasErrors = field.errors.length > 0
            const errorId = `${field.name}-errors`

            return (
              <div className="field">
                <label htmlFor={field.name}>Age</label>
                <input
                  id={field.name}
                  name={field.name}
                  type="number"
                  min="0"
                  inputMode="numeric"
                  value={field.value}
                  aria-invalid={hasErrors}
                  aria-describedby={hasErrors ? errorId : undefined}
                  onBlur={field.handleBlur}
                  onChange={(event) =>
                    field.handleChange(
                      event.target.value === ''
                        ? 0
                        : event.target.valueAsNumber,
                    )
                  }
                />
                {hasErrors ? (
                  <div id={errorId} className="field-errors" role="alert">
                    {field.errors.map((error) => (
                      <p key={error.message}>{error.message}</p>
                    ))}
                  </div>
                ) : null}
              </div>
            )
          }}
        </form.Field>

        <div className="actions">
          <form.Subscribe
            selector={(formState) => [
              formState.canSubmit,
              formState.isSubmitting,
            ]}
          >
            {([canSubmit, isSubmitting]) => (
              <button type="submit" disabled={!canSubmit || isSubmitting}>
                {isSubmitting ? 'Submitting' : 'Submit'}
              </button>
            )}
          </form.Subscribe>
          <button
            type="reset"
            className="secondary"
            onClick={(event) => {
              event.preventDefault()
              form.reset()
            }}
          >
            Reset
          </button>
        </div>
      </form>

      <form.Subscribe selector={(formState) => formState.values}>
        {(values) => (
          <output className="values-preview">
            {JSON.stringify(values, null, 2)}
          </output>
        )}
      </form.Subscribe>
    </main>
  )
}