react-jsonschema-form icon indicating copy to clipboard operation
react-jsonschema-form copied to clipboard

Allow for debouncing live validation (liveValidate)

Open michal-kurz opened this issue 2 years ago • 14 comments

Prerequisites

What theme are you using?

other

Is your feature request related to a problem? Please describe.

The liveValidate prop performs quite well for one-off state changes in our project when using @rjsf/validator-ajv6 (although not with @rjsf/validator-ajv8 for some reason), but it lags when typing on slow machines.

Describe the solution you'd like

This would seemingly be solved by giving implementator a choice to debounce the validation (so it would only initiate live validation once the user has not typed anything for X milliseconds).

Describe alternatives you've considered

I tried achieving the same effect by turning off liveValidation and calling a debounced instance of Form.validateForm() inside onChange, but it lead to very confusing results for me:

  1. it appears that liveValidation actually functions slightly differently from Form.validateForm() in what/how it validates - as if liveValidation only validated inputs that were actually being rendered, while Form.validateForm() always validating all of formData against all of schema - or something in that ballpark. I was getting very different results between these two, and Form.validateForm() turned out to be terribly incompatible with our legacy code.
  2. Form.validateForm() triggers onError, while liveValidation doesn't. This is a big issue for us, because we use custom logic for un-collapsing collapsed form sections (custom widgets) and scrolling to invalid fields onError. When using Form.validateForm() onChange to simulate live validation, this makes our form scroll all over the place all the time. De-coupling Form.validateForm() and onError (maybe allowing for Form.validateForm({ triggerErrorHandler: false })) might help our use-case greatly.

It also might be possible to manually achieve this by wrapping @rjsf/validator-ajv6 into a custom validator and using debounce somewhere in the way - but I have no idea how to approach this. Do you think that would be possible?

Thank you for your consideration 🙏

michal-kurz avatar Apr 19 '23 22:04 michal-kurz

@michal-kurz If you look at the implementation of how liveValidate works, you can see that it calls the validate() function on the form with just the changed data (as you surmised). I agree that trying to debounce things yourself would lose the state updating happens in the live validate code in the onChange() handler in Form. This sounds like a nice feature for someone (like you?) perhaps to implement. We maintainers probably have a few hours a week to work on issues like this and we LOVE to get help from you all using the library. I think that making the liveValidate flag also take an additional object value containing the debounceThreshold would be the simplest solution. (i.e. liveValidate?: boolean | { debounceThreshold: number };)

heath-freenome avatar Apr 21 '23 16:04 heath-freenome

@heath-freenome Thank you very much for you feedback and pointing me to the right direction. I have a lot of work and personal stuff to do right not, but I will get back to it in about two weeks and try to implement this. I made myself a ticket so I don't forget :)

michal-kurz avatar Apr 22 '23 11:04 michal-kurz

I just want to affirm that I'm still planning to do this! I will get to it during next workweek :)

michal-kurz avatar May 03 '23 19:05 michal-kurz

@heath-freenome I apologize for the delay, I was able to get to it much later than I expected.

I made a proof of concept for the invalidation debouncing, and it can be found here: https://github.com/michal-kurz/react-jsonschema-form/pull/1/files#

It's in no way polished, and in its current state breaks some other functionality, but I wanted to consult it with you to assess whether it's worth continuing on this route. Would you please look at it? It this roughly acceptable as a concept, or did you imagine something else?

Here are my takeaways:

  1. In the current main branch and Playground package, when liveValidation is enabled, this.validate() triggers three times on every keystroke
    1. this.onChange() -> this.getStateFromProps() - here, the validation result doesn't even get used, and gets later overriden - it just seems to be called as a byproduct of other invokers of this.getStateFromProps() using the validation result elsewhere
    2. inside this.onChange() body, when this.validate() gets called directly with result of this.getStateFromProps()
    3. in componentWillReceiveProps() afterwards - I suppose this could be solved from the outside by memoizing the instance, but doing that reliably comes with a whole another set of challenges when working with large and complex forms.
  2. Since my main point is improving the performance of the form, I definitely want to run validation zero times on every keystroke, when in debounced mode (and only eventually once debounce kicks in) - only debouncing the main trigger while leaving the other two in would be missing the point.
  3. I assume that the rest of the form's logic (outside of live validation) should remain unchanged, and and only live validation should be debounced. This way, error states (errors, errorSchema, validationErrors, validationErrorSchema) are no longer always consistent with the rest of the form, when liveValidate is enabled, but only eventually consistent - even when no debounceThreshold is provided. This seems OK, since a. this is how form works outside of liveValidate and b. this kindof seems to be the point of this change - but I wanted to check with you to make sure.

michal-kurz avatar May 23 '23 19:05 michal-kurz

How do we go on with this, @heath-freenome ?

I initially had plans with this - I was hoping for massive performance gains, looking to potentially delegate debounced (async) liveValidation onto a separate worker to free up the main thread. But I've since uncovered that the performance bottlenecks with our use-cases lie elsewhere. Do you think it's worth-while to keep pursuing this? Maybe you have some knowledge suggesting that this would actually massively help with some very common use-cases - otherwise I'd say this is probably not worth it.

There's one thing I haven't yet tested and included in my consideration though: We are currently running on @rjsf/validator-ajv6, because ajv-8 gives us terrible performance. Do you know why this could be? If this difference is caused by liveValidate calls, this may be such "very common use-case" I described above :)

michal-kurz avatar Jun 16 '23 12:06 michal-kurz

This is very sorely needed. I have a form with several dozen fields and complex validation logic, and I need live validation but doing it on every key stroke causes visible lag.

A hack I'm using at the moment is replacing onChange in every custom field e.g. StringField with a debounced version

  const oldOnChange = onChange;
  onChange = (...args) => {
    oldOnChange(...args);
    formContext.revalidate.run(); // debounced version of ref.current.validateForm() passed to formContext
  };

This works to reduce lag but is very repetitive and manual and hacky.

Edit:

I should be clear that I need to revalidate the entire form on any change due to what I think is a reactivity bug (or just a hole in the architecture, maybe related to #3838) where two field values' validation depend on each other via a custom AJV keyword, e.g. price_min must be less than price_max, and when one field's value changes the other field's errors will not re-render appropriately.

So maybe it is a more niche use-case and less commonly needed.

vincerubinetti avatar Aug 25 '25 19:08 vincerubinetti

@vincerubinetti are you using v5 or v6? A recent v6 change will make it easier to potentially debounce live validate since we are now queuing up rapid changes, so you could potentially detect that there are queued changes and could skip validation.

heath-freenome avatar Aug 27 '25 01:08 heath-freenome

@michal-kurz So sorry for not responding to your questions. I think that you may be able to take advantage the latest changes in v6 where we queue up change request in the Form... Theoretically, you could detect when there are pending changes and skip live validation until there are no more

heath-freenome avatar Aug 27 '25 01:08 heath-freenome

Also, @vincerubinetti and @michal-kurz I made a small live validation optimization in this PR. Give it a try once we release it and see how that works for you

heath-freenome avatar Aug 27 '25 01:08 heath-freenome

@vincerubinetti The optimizations have been released in 6.0.0-beta.15. Let us know if that is helping

heath-freenome avatar Aug 27 '25 23:08 heath-freenome

@vincerubinetti The optimizations have been released in 6.0.0-beta.15. Let us know if that is helping

I tested this update and it has had some adverse effects when using the props formData and onChange in the form. We have a few custom widgets that depend on several other properties in a form which we can only track reliably using an external state, after this change the validation happens but it gets overwritten. I'm assuming it's happening because our onChange writes the new formData to the external state which in turn changes the forms formData and some condition happens here.

Fronix avatar Sep 09 '25 08:09 Fronix

Yes, we changed how onChange() handling works so that it queues up the state change on a per-field basis rather than combining the formData at each level of the schema hierarchy. If you have a custom widget that is updating formData, either call onChange() per field (now supported and queued). Sounds like your external state causes formData to update outside of the Form itself. I'm assuming that you are controlling the formData from outside?

heath-freenome avatar Sep 09 '25 20:09 heath-freenome

Yes, we changed how onChange() handling works so that it queues up the state change on a per-field basis rather than combining the formData at each level of the schema hierarchy. If you have a custom widget that is updating formData, either call onChange() per field (now supported and queued). Sounds like your external state causes formData to update outside of the Form itself. I'm assuming that you are controlling the formData from outside?

Yes we are, I'm in the process of rewriting our implementation and just trying to find the right version to not miss out on new features that we can use. I will patch on our side for now to avoid this issue, but at least this might some one else know where the issue lies.

Fronix avatar Sep 09 '25 20:09 Fronix

@Fronix @michal-kurz With the release of 6.x we've added the ability to defer live validation until the blur of the field. I HOPE this gives you enough performance savings... Plus it effectively DOES the live validate at the stage you likely really want it to happen, when the user is done editing the field. I will close this unless there is a disagreement with this. Thanks

heath-freenome avatar Nov 12 '25 01:11 heath-freenome