Crustack
@crustack/form

Validation

Learn how to manage synchronous and asynchronous validation.

Crustack form uses field-level validation instead of form-level schemas. This approach prevents duplicating validation logic and keeps validation rules close to their respective fields.

Field-level validation simplifies complex scenarios like multi-step forms and conditional fields by only validating mounted fields. This makes the form more maintainable and performant.

Can I use a validation library?

Yes, you can use any validation library that is compliant with the Standard Schema Specification.

When is validation performed?

Validation occurs:

  • whenever form values change
  • when a submit attempt is made
  • when onSubmit resolves/rejects

This ensures that dependent fields are updated based on the new values, maintaining form consistency.

When is the error message displayed?

Error messages appear when a field is touched and the validate function returns an error string. A field becomes touched either through the setFieldTouched function or when submitting the form.

Sync Validation

Basic validation

Simple field validation:

function () {
  return (
    <.
      ={{ : '', : '' }}
      ={}
    >
      <.>
        <.
          ="title"
          ={() => . < 5 && 'Too short'}
        >
          < ="Title" />
          <. />
        </.>

        <.
          ="description"
          ={() => . < 20 && 'Too short'}
        >
          < ="Description" />
          <. />
        </.>

        < ="Submit" />
      </.>
    </.>
  )
}

Dependent fields

Validate a field based on the value of another field:

function () {
  return (
    <.
      ={{ : '', : '' }}
      ={}
    >
      <.>
        <.
          ="title"
          ={(, ) => {
            if (. < 5) {
              return 'The title is too short'
            }
            if (. > ..) {
              return 'The title should not be longer than the description'
            }
          }}
        >
          < ="Title" />
          <. />
        </.>

        <.
          ="description"
          ={(, ) => {
            if (. < 20) {
              return 'The description is too short'
            }
            if (. < ..) {
              return 'The description should be longer than the title'
            }
          }}
        >
          < ="Description" />
          <. />
        </.>

        < ="Submit" />
      </.>
    </.>
  )
}

Field Arrays

Validation can also be applied to field arrays to have one error message for all fields in the array.

const  = ['apple', 'banana', 'orange', 'pear', 'pineapple', 'strawberry']

function () {
  return (
    <. ={{ : [] }} ={}>
      <.>
        <.
          ="fruits"
          ={() =>
            . < 3 && 'You must choose at least 3 fruits'
          }
        >
          {.((, ) => (
            <. ={`fruits.${}`} ={`fruits.${}`}>
              < ={} />
            </.>
          ))}
          {/* Display the field array error message */}
          <. />
        </.>

        < ="Submit" />
      </.>
    </.>
  )
}

function ({  }: { : string }) {
  const  = .()
  return (
    <>
      <.>
        <
          ="checkbox"
          ={..()}
          ={() => {
            if (..) {
              .(({  }) => [..., ])
            } else {
              .(({  }) => .(() =>  !== ))
            }
          }}
        />
      </.>
      <.>{}</.>
    </>
  )
}

Async Validation

Submission based async validation

Validate fields based on the submission state.

import {  } from '@crustack/form'

type  = { : string }
type  = { ?: string }

const  = <, >()

// Fake API call that rejects with a SubmissionError
async function () {
  return .({ : 'invalid email' })
}

export function () {
  return (
    <. ={{ : '' }} ={}>
      {() => (
        <.>
          <.
            ="email"
            ={() => {
              // if this field's value is the same as the submission's values, return the submission error
              if ( === ..?.) {
                return ..?.
              }
              // otherwise, validate the email on the client
              return ()
            }}
          >
            < ="Email Address" />
            <. />
          </.>

          < ="Submit" />
        </.>
      )}
    </.>
  )
}

Field-level async validation

Check if an email is already used before subscribing.

import {  } from '@tanstack/react-query'
import {  } from 'react'
import {  } from '@crustack/form'
import {  } from '@crustack/utils'
import {  } from '@1hook/use-debounce-value'

// define the form
const  = <{ : string; : string }>()

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

function () {
  const { , ... } = .()
  const  = .('email')
  const  = (, 500)
  const  =  !== 
  /**
   * Using the synchronous email value instead of the debounced value
   * in order to avoid using stale cached results.
   * Using debouncedEmail in the queryKey will output the cached result
   * of the previous debouncedEmail while isDebouncing.
   */
  const  = ({
    : ['email', ],
    // disable the query if there is a sync error, or the email is being debounced
    : ! && !(),
    // Avoid throwing the validation response: tanstack-query doesn't cache errors
    : async () => {
      // touch the field to show the validation result
      .('email', true)
      await (1000)
      if ( === 'valid@email.com') return ''
      return 'Email already exists'
    },
  })

  /**
   * Revalidate the form when the async validation is completed.
   * This will update all displayed error messages
   */
  (, [
    .,
    .,
    ,
  ])

  const  =
    // no pending state when the sync validation fails
    !() &&
    // otherwise show pending state
    ( || .)

  function (: string) {
    return (
      // sync validation
      () ||
      // async validation, since we don't throw the validation result
      // to best use the cache, we use the `data` property
      . ||
      // async validation failed (eg. "Failed to fetch")
      (. &&
        'An unexpected error has occurred, please try again later.')
    )
  }

  return (
    <. ="email" ={}>
      < ="Email Address" />

      {!! && (
        // Prevent the form submission when the pending state is displayed
        <.>
          < />
        </.>
      )}

      <. />
    </.>
  )
}

On this page