There, two similar form views are rendered:
I am implementing a more complex form logic, where each submit action on one form triggers the rendering of the next form. the last form is an Overview, that solely displays all previously entered data, thus it merges the individual data sets of each single form view. In the last Overview form, I can choose to submit all data or cancel the submission. On cancel, the first form gets displayed again and the form data of all forms gets cleared (in the JSFiddle, the custom form data is used).
The implementation is done as a simple state machine, logic wise quite similar to the one in the JSFidddle.
In each of my form views, several fields are validated on submit, such as email address, integer fields and so on. If field validation fails, an error message gets displayed for that/those field(s). So far, this is expected behaviour.
When I finally enter the last Overview form, the last error message that occurred in any of the previous forms reappears below the corresponding field, even though the field data is correct (otherwise, the submit of that form would not have been succeeded).
In a similar fashion, if I the re-initiate the process of filling in the forms (by canceling submission in the Overview), and then enter the one form where previously the last validation error occurs, this error message reappears on the corresponding field again, even though the field has been cleared or contains valid default data. Thus I can submit this form anyways, despite the error message.
Searching the issue list, I found a similar bug, but that has already been fixed: #195
Even though I provide custom submit buttons for each form view (as in the JSFiddle, in order to change the button label, and to provide to buttons in the Overview), I provide a onSubmit callback as form property, thus I assume that the internal submission logic of the form library is not altered. My callback gets called on button click anyway, so I assume that onSubmit of the Form class gets triggered correctly on submit-button click.
In Form::onSubmit() I can also see, that the internal "errors" and "errorSchema" state gets reset, but for some reason, this is not reflected in the rendered forms.
The workaround that can be seen in the JSFiddle tries to manually reset "error" and "errorSchema" states of the Form class.
In order to do that, I inherit from the Form class and extend my class with a react child context object. My own state machine (the parent of all the rendered forms) "renders" this subclass and provides the parent context (just a simple string that holds the current state name).
Changing the context in the state machine on submission of each single form will trigger a react call to "componentWillReceiveProps". My subclass overwrites this method. There, it calls the parents Form::componentWillReceiveProps first before it resets the error states of its parent.
In the fiddle, reproduce the following steps in the upper form in order to see the actual buggy behaviour. In the lower form, the same steps should work without error message propagation.
Steps in upper form:
When field data validation errors has been fixed in a form A and its submit succeeded, I do not expect to see any previous errors message in the next form B or when I enter form A again.
The last validation error message in a form view gets propagated to the next form(s). The message then shows up in any further form, that uses the same schema structure, ie. the same nesting level with the same field key(s). If the form where an validation error has been displayed previously is rendered again, this previous error message reappers.
react-jsonschema-form:
CDN version linked in documentation (probably 0.40.0)
https://unpkg.com/react-jsonschema-form/dist/react-jsonschema-form.js
react: 15.3.1
babel: 6.15.0
babel preset: react, es2015
Thanks for the detailed report. The examples are really complex to follow, also I'm seeing props used never defined anywhere, like this.props.templates.defaultData, so I'm wondering about the actual source of the issue you describe (I'm not saying it's not related, but still).
What would be super helpful is to try to isolate and reduce the problem to its simplest expression; why would the errors be persisted? it may be an issue where these aren't cleared once the form is submitted then reedited, or something like this.
Also, possibly related or possibly not, I'm seeing you use a lot the foo = {...bar} pattern, which may be used to clone objects but keep in mind this doesn't work recursively, so you'll simply clone the first level properties but nested ones will still point at their respective references. I've seen many bugs due to just this in complex form workflows, so you may triple check there's no mutation involved in your object props.
Again, sorry for not being more immediately helpful here, but I'm really lacking time to spend on complicated issues like the one you've kindly reported. Please help me by reducing the cognitive context around the problem and focus on the actual smallest bits of code involved so I can immediately grasp what's in play here.
Thanks for your understanding :)
hi n1k0, txs for your reply.
yes, the example was too complicated. I stripped down the fiddle to the basics (only two forms with one field, no EC6 stuff, no custom buttons, some hardcoded properties). the code in the new JSFiddle.
I also removed the workaround code, so that only the erroneous form is displayed. The error message behaviour is the same as in my initial description, my local workaround also works for this simplified code.
The reproduction steps are simple:
I will also debug this code a bit. my guess would be that the internal error and errorSchema states of the Form class are not reset correctly on form submission. I will let you know about any findings.
I've been experiencing a similar issue with broken form on resubmission lately, we're probably on something here. Will investigate tomorrow.
I have a similar issue, though I'm able to re-submit the form (but validation errors won't disappear). This error only happens when I pass a custom method to the onSubmit prop. I took a look to the source code and it seems like the error could be happening here:
// Form.js
if (_this.props.onSubmit) {
_this.props.onSubmit(_this.state);
}
_this.setState({ status: "initial", errors: [], errorSchema: {} });
It behaves like if the setState method wasn't being called at all.
I am experiencing the exact same issue.
Got an error, make a change, then onSubmit is executed with { errors.length > 1 & my formData with the data not matching the error (in my case a simple required that is filled in the formData).
Hey there, I was away for some time, but back again and recently started to debug this issue.
After debugging, my assumption is that this issue might be related to _React_component lifecycle.
In order to understand the order of methods being called in/by _Form_ and _React_ I inherited from the _Form_ class, and put logging all over the place.
Observation
I spare you all the logging details but the situation might basically be as follows (for reference, take a look at Form::onSubmit(), Form::componentWillReceiveProps() and Form::getStateFromProps() in Form.js):
1) I enter a wrongly formated email address and press submit
2) _Form::onSubmit()_ gets triggered, in there
2.1) validation happens and returns an error object containing the error about wrong email format
2.2) _Form::setState()_ is called, with the new error object
2.3) _Form::onSubmit()_ returns without calling _props.onSubmit()_ callback
Afterwards, at some point of time, React will change the Form's _state_ member and adds the error from _Form::onSubmit()_.
3) Now I correct the email address and press submit again
4) _Form::onSubmit()_ gets triggered, in there
4.1) validation happens, this time no error gets returned, as email format is correct
4.2) the external callback _props.onSubmit()_ is called, in there
4.2.1) in there, the state of the parent object (in my case) is changed. That will trigger _parent.render()_ at some point of time...
4.2.2) the external _onSubmit()_ callback returns
4.3) as last action, _Form::onSubmit()_ resets the Form's errors by calling _Form::setState()_ with proper values (empyt list and empty object)
4.4) _Form::onSubmit()_ returns
Conclusion
Basically two actions are happening in parallel, and depending on which action is performed first, the result is as expected or not (as in our case)
During _Form::onSubmit()_:
As React does not set the state of a component immediately in _setState()_, but at an unknown point of time, we cannot foresee if the setting of the Form's error state will take place before or after the setting of the parents state.
In our case, it seems that the parent component is receiving its state update first and therefore also a call to its _render()_ method. The parents _render()_ method then renders a new Form by passing new properties to the _Form_.
This results in a call to _Form::componentWillreceiveProps()_, as defined by the React Components Lifecyle, while the Form's pending error state change has still not been propagated to the Form's _state_.
_Form::componentWillreceiveProps()_ is implemented as follows:
// Form.js
componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromProps(nextProps));
}
and in _Form::getStateFromProps()_ we can find the following lines of code:
// Form.js - getStateFromProps()
const {errors, errorSchema} = mustValidate ?
this.validate(formData, schema) : {
errors: state.errors || [],
errorSchema: state.errorSchema || {}
};
If we assume that the _state.errors_ has not been updated yet, it still holds the previous error. _Form::getStateFromProps()_ is therefore returning a new state object that is partly composed of the newly available properties and partly of the old state (in particular _state.errors_ and _state.errorSchema_).
A new state update is then scheduled via _Form::setState()_ and adds to the Form's previous state updates, so to say, on top of the pending one performed in _Form::onSubmit()_.
So, the new state holds an error object that will 'overwrite' the previously changed error from _Form::onSubmit()_ on the next occasion, when _React_ merges and propagates all pending state changes to the Form's _state_.
Therefore, the error persists, even though the error object has been reset.
I hope I could express everything more or less clearly as it is quite complicated to lay out concurrency issues in an understandable way in written text.
What's next?
From my understanding now, I think that it is not important if a _Form_ is a child of a parent _React_ component. It only seems to matter, that the Form's external _onSubmit()_ callback (stored in _props.onSubmit_) is available and implements a logic that somehow causes a new rendering of a _Form_, by passing new properties to the _Form_.
_React_ then triggers the _Form::componentwillReceiveProps()_ method and the situation might be as described above.
Looking at the locations where _Form::getStateFromProps()_ is being called from, it appears that only the Form's _constructor()_ and _Form::componentwillReceiveProps()_ make use of that function.
Would it be correct to assume that there is no error at all in the constructor and when receiving new properties, the _Form_ can neglect all previous errors, as it is rendered according to those new properties, thus previous errors do not matter anymore?
In that case, _Form::getStateFromProps()_ could probably just return empty error objects instead of using the state errors.
\\ Form.js - getStateFromProps()
const {errors, errorSchema} = mustValidate ?
this.validate(formData, schema) : {
errors: [],
errorSchema: {}
};
@quilombo Not sure the solution exposed at the end of your response make sense.
In my case, I found that this "error state reset" end up (I added a callback to see when) after the componentWillReceiveProps, which call getStateFromProps and read the state (outdated) at the beginning of the call (so the validation made is using outdated state and produces false positives...)
Continuing my investigation for a fix...
Found something that works for me:
Replace this:
if (this.props.onSubmit) {
this.props.onSubmit(this.state);
}
this.setState({status: "initial", errors: [], errorSchema: {}});
By this
this.setState({status: "initial", errors: [], errorSchema: {}}, () => {
if (this.props.onSubmit) {
this.props.onSubmit(this.state);
}
});
Tell me how it is for you.
Hi @MoOx Thanks for this solution, this one worked for me :) Hope it will have no weird side effects,
@MoOx are there any plans to update the onSubmit implementation with the edit you suggested above to invoke the user-provided onSubmit handler after the error state has been reset (e.g., to prevent the Form from being re-rendering with stale error messages)?
For now I'm sub-classing Form and overriding the onSubmit method with the workaround from above but it'd be nice if this made it into a release version. Since the onSubmit property is invoked after validation occurs it seems reasonable to remove errors from the Form state prior to the user onSubmit handler being called right?
@n1k0, could you elaborate on the current Form#onSubmit behavior? It looks like it's trying to set its state twice, and calling the props.onSubmit in between. Is this some React sorcery that I don't know about? Should we be calling the props.onSubmit in callbacks as suggested by the commenters on this issue?
I don't have the context in mind anymore but from what I'm reading what @MoOx suggests makes sense, we should probably open a PR with that patch and see how it goes.
I believe this is fixed by #844.
Most helpful comment
Found something that works for me:
Replace this:
By this
Tell me how it is for you.