Crustack
@crustack/form

Basic concepts

Learn the core concepts and principles behind Crustack Form to better understand how the library works.

defineForm

The defineForm function is the main entry point for creating a form. It takes three generic parameters:

  • FormValues: The type of the form values.
  • SubmissionError: The type of the submission error, thrown/rejected by onSubmit.
  • SubmissionData: The type of the submission data, returned/resolved by onSubmit.

It returns fully type-safe form components and hooks.

Example:

import {  } from '@crustack/form'

type  = { : string; : string }
type  = Error
type  = string

export const  = <, , >()

Root

The Root component provides the form context to its children and handles the form submission. It expects:

  • the initialValues that should match the FormValues type passed to defineForm.
  • the onSubmit function that should either resolve SubmissionData or reject SubmissionError.

Optionally the Root component accepts a render prop pattern where the children prop can be a function that receives the form context, enabling direct access to form state and methods.

Example:

async function (: ) {
  if (.() > 0.5) {
    return 'Form submitted succesfully'
  }
  throw new ('Something went wrong')
}

function () {
  return (
    <.
      ={{ : '', : '' }}
      ={}
    >
      {() => <.>...</.>}
    </.>
  )
}

Form

Renders an accessible <form> element. The Root component can contain several Form components.

Example:

function () {
  return (
    <.
      ={{ : '', : '' }}
      ={}
    >
      <.>First form</.>
      <.>Second form</.>
    </.>
  )
}

Field

The Field is the main building block of a form. It represents a single input field and its state. A field is composed of a Control (the input itself), a Label, a Description and an ErrorMessage.

It expects:

  • a name that should match a key of the FormValues
  • a validate function that should return an error message if the field is invalid.
  • a disabled prop that propagates the disabled state and ARIA attributes to all Field children, providing a more consistent accessibility experience than disabling individual controls

Example:

<form.Field name="title" validate={(title) => title.length < 5 && 'Too short'}>
  <form.Label children="Title" />
  <form.Description children="Add a title to your new todo" />
  <form.Control>
    <input
      value={ctx.getFieldValue('title')}
      onChange={(e) => ctx.setFieldValue('title', e.target.value)}
      onBlur={() => ctx.setFieldTouched('title', true)}
    />
  </form.Control>
  <form.ErrorMessage />
</form.Field>

Managing the State

The state of the form can be managed using useFormCtx.
The state of a single field can be managed using useFieldCtx.
The state of a field array can be managed using useFieldArrayCtx.

useFormCtx can be used to read/write field values and touched states. The error state is read-only, managed by each field's validate function.

const  = ()
// Read the "title" field
.('title')
// Write the "title" field
.('title', 'New title')
// Read the "title" touched state
.('title')
// Touch the "title" field
.('title', true)
// Read the "title" error
.('title')

useFieldCtx reads the state of the closest parent Field component.

const  = ()
// Read the value
.
// Writethe value
.('New title')
// Read the touched state
.
// Touch the field
.(true)
// Read the error state
.

useFieldArrayCtx does the same as useFieldCtx but for field arrays.

Validation

Validation occurs automatically for all rendered fields that have a validate function. This approach maintains consistency across the form and handles dependent field validation without requiring additional configuration.

The validate prop can be a function or a Standard Schema.

The validation result appears in the ErrorMessage component when two conditions are met:

  1. The validate function returns an error message
  2. The field has been touched by the user

All fields are touched when a submit attempt is made.

Functional example:

<Field name="title" validate={(title) => title.length < 5 && 'Too short'}>
  <Label>Title</Label>
  <Control>
    <input
      onChange={(e) => ctx.setFieldValue('title', e.target.value)}
      onBlur={() => ctx.setFieldTouched('title', true)}
    />
  </Control>
  <ErrorMessage />
</Field>

Using zod:

<Field name="title" validate={z.string().min(5, { error: 'Too short' })}>
  <Label>Title</Label>
  <Control>
    <input
      onChange={(e) => ctx.setFieldValue('title', e.target.value)}
      onBlur={() => ctx.setFieldTouched('title', true)}
    />
  </Control>
  <ErrorMessage />
</Field>

The Root's onSubmit function is called when the form is submitted without errors.

On this page