#onBlur validator not re-running on submission after first submit

15 messages · Page 1 of 1 (latest)

inland pier
#

Hello,

I'm running into a tricky situation with complex validation, and was wondering if I'm missing some important API. I'm trying to do price string validation, so I have a "draft price" Zod schema and then a standard "price".

Draft price allows inputs like 9., which could feasibly be part of a valid price string, while the final validator ensures that the format is fully correct.

I'm running the draft price validator onChange, and then final price validator onBlur. This works well for the most part, but I've been running into an edge case where the onBlur validator result won't go away, even once the price is corrected.

As an example, Say I'm using this (slight modification of the example from the docs):

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

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

export const zPriceString = () =>
  z
    .string()
    .trim()
    .regex(/^\d+(\.\d{1,2})?$/, { error: 'Invalid price' })

export const zPriceStringDraft = () =>
  z
    .string()
    .trim()
    .regex(/^\d*(\.\d{0,2})?$/, 'Invalid price')

export function FieldInfo({ field }: { field: AnyFieldApi }) {
  return (
    <>
      {field.state.meta.isTouched && !field.state.meta.isValid ? (
        <em>{field.state.meta.errors[0]}</em>
      ) : null}
      {field.state.meta.isValidating ? 'Validating...' : null}
    </>
  )
}

export const Test = () => {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      price: '',
    },

    onSubmit: async ({ value }) => {
      // Do something with form data
      console.log(value)
    },
  })
#
return (
    <div>
      <h1>Simple Form Example</h1>
      <form
        onSubmit={(e) => {
          e.preventDefault()
          e.stopPropagation()
          console.log('FORM SUBMIT')
          form.handleSubmit()
        }}
      >
        <div>
          <form.Field
            name="price"
            validators={{
              onBlur: ({ value }) => {
                const { success } = zPriceString().safeParse(value)
                return success ? undefined : 'Invalid price onBlur'
              },
              onChange: ({ value }) => {
                const { success } = zPriceStringDraft().safeParse(value)
                return success ? undefined : 'Invalid price onChange'
              },
            }}
            children={(field) => (
              <>
                <label htmlFor={field.name}>Price:</label>
                <input
                  className="border-black border"
                  id={field.name}
                  name={field.name}
                  value={field.state.value}
                  onBlur={field.handleBlur}
                  onChange={(e) => field.handleChange(e.target.value)}
                />
                <FieldInfo field={field} />
              </>
            )}
          />
        </div>
        <form.Subscribe
          selector={(state) => [state.canSubmit, state.isSubmitting]}
          children={([canSubmit, isSubmitting]) => (
            <>
              <button type="submit" disabled={false}>
                {isSubmitting ? '...' : 'Submit'}
              </button>
              <button
                type="reset"
                onClick={(e) => {
                  // Avoid unexpected resets of form elements (especially <select> elements)
                  e.preventDefault()
                  form.reset()
                }}
              >
                Reset
              </button>
            </>
          )}
        />
      </form>
    </div>
  )
}
#

If I type 9. into the price field, this is correctly allowed due to the permissive onChange validator. If I try to submit with 9., onBlur validator fires and an error correctly appears.

However, if I then update the price to 9.99, the error from the onBlur validator persists. Even if I click Enter to resubmit the form, the onBlur validator apparently does not re-run, so even the correct input of 9.99 is marked as invalid due to the outdated onBlur error.

Is this the intended behavior in Tanstack Form? If so, is there a built-in way to work around this? I would have expected that if onBlur validator fires on first submit, it would fire again on each subsequent submit. However, that's not the case, and I'm wondering if that's intended behavior or a bug?

inland pier
#

As a follow-up, I experimented a little bit and the behavior with onSubmit validators behaves far more predictably than onBlur. I think this may be a bug.

For instance, when I have both onSubmit and onChange validator, then submit an invalid value, an onSubmit error is added to errorMap. However, if I type in the field and trigger onChange validator, the onSubmit error is immediately removed from the errorMap. This results in much better UX, where typing in an invalid field will instantly begin using the onChange handler again.

In contrast, onBlur errors will stay on the errorMap even once onChange validator fires and passes validation. It doesn't feel like this would be the intended behavior.

light patio
#
  1. onBlur only runs on a blur, so it was waiting for you to blur a field
  2. The handleSubmit saw that there's still errors that have not yet been resolved and exited early
#

if you want to force the validation again, then try the snippet with canSubmitWhenInvalid: true

#

it'll force another validation to occur even if there's still errors present

inland pier
#

Is there a good reason for this design? It seems like it would be more straightforward for all validators to fire when submit is attempted before "are there any errors preventing me from submitting" logic fires.

#

Also for the record, I was able to switch to onSubmit logic instead of onBlur, and everything works spectacularly. But onBlur behavior is really suboptimal right now.

For example, when you have an onBlur validator and an onChange validator, the onChange validator doesn't cause the onBlur validator errors to disappear. Whereas with onSubmit this works seamlessly. This led to me threading an isFocused prop just to track whether the blur error should be displayed or not, when ideally this logic would belong to the form lib (and it does work perfectly / as expected for onSubmit)

light patio
#

whereas with blur validation, that check will happen in the next blur, because that's not blocked by existing errors

#

the reason submissions exit early with errors present is to avoid endpoint calls or expensive validation to run when the user hasn't fixed things yet

inland pier
#

I see what you mean. To be clear, I wasn't suggesting that form should be able to fully submit (i.e. make API request) when there's still an existing error. I was only saying that it seems cleaner to revalidate every time the user attempts to submit the form.

This would circumvent the issue where onBlur error won't go away. Optimal UX would probably be for the onBlur logic to be revalidated so that users could submit without having to blur the field (which would be a bit strange).

Anyway, I appreciate the help. Hope you guys will consider this tweak, because I think it'd make the lib a little bit better, but regardless TF is awesome.