Allow for debouncing live validation (liveValidate)
Prerequisites
- [X] I have read the documentation
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:
- it appears that
liveValidationactually functions slightly differently fromForm.validateForm()in what/how it validates - as ifliveValidationonly validated inputs that were actually being rendered, whileForm.validateForm()always validating all offormDataagainst all ofschema- or something in that ballpark. I was getting very different results between these two, andForm.validateForm()turned out to be terribly incompatible with our legacy code. Form.validateForm()triggersonError, whileliveValidationdoesn'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 fieldsonError. When usingForm.validateForm()onChangeto simulate live validation, this makes our form scroll all over the place all the time. De-couplingForm.validateForm()andonError(maybe allowing forForm.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 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 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 :)
I just want to affirm that I'm still planning to do this! I will get to it during next workweek :)
@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:
- In the current
mainbranch and Playground package, whenliveValidationis enabled,this.validate()triggers three times on every keystrokethis.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 ofthis.getStateFromProps()using the validation result elsewhere- inside
this.onChange()body, whenthis.validate()gets called directly with result ofthis.getStateFromProps() - 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.
- 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.
- 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, whenliveValidateis enabled, but only eventually consistent - even when nodebounceThresholdis provided. This seems OK, since a. this is how form works outside ofliveValidateand b. this kindof seems to be the point of this change - but I wanted to check with you to make sure.
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 :)
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 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.
@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
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
@vincerubinetti The optimizations have been released in 6.0.0-beta.15. Let us know if that is helping
@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.
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 changed how
onChange()handling works so that it queues up the state change on a per-field basis rather than combining theformDataat each level of the schema hierarchy. If you have a custom widget that is updatingformData, either callonChange()per field (now supported and queued). Sounds like your external state causesformDatato update outside of theFormitself. I'm assuming that you are controlling theformDatafrom 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 @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