react-jsonschema-form
react-jsonschema-form copied to clipboard
`props.onChange` ignored if i update other state in the parent component from a custom widget
Prerequisites
- [X] I have searched the existing issues
- [X] I understand that providing a SSCCE example is tremendously useful to the maintainers.
- [X] I have read the documentation
- [X] Ideally, I'm providing a sample JSFiddle, Codesandbox.io or preferably a shared playground link demonstrating the issue.
What theme are you using?
core
Version
4.x, 5.x
Current Behavior
I have to use flushSync
or setTimeout
to flush my state changes in my custom widgets before calling props.onChange
Expected Behavior
I should not have to call flushSync
but simply update my state and then call props.onChange(value)
in my custom file upload widget.
Steps To Reproduce
- Go to https://codesandbox.io/s/wonderful-wave-sp997w?file=/src/App.js
- See console,
formData
andfile
are printed periodically - Choose a file
- Verify that
formData
contains a file path (fakepath) andfile
is a reference to aFile
- Comment out the
flushSync
line and uncomment the the line that doessetFile(file)
without flushSync - Reload the page and do step 2, 3 and 4 again.
- Verify that only
file
is set,formData
is empty - it is as if the change was never received.
Environment
No response
Anything else?
I am genuinely curious to why this is happening. I set up something similar without rjsf (where a child component would call 2 callbacks that both update the parent state) and there was no issue.
@linde12 The formData
passed to the custom widget isn't the same object as the formData
state stored in the top-level Form
component. There's a complex 'state engine' that handles all of those changes and transforms the data to pass just a subset to each field and widget so it only sees the data it is concerned with. As a result, your custom widget can't cleanly maintain its own state and can only use the onChange
handlers provided via props to change the formData.
@linde12 The
formData
passed to the custom widget isn't the same object as theformData
state stored in the top-levelForm
component. There's a complex 'state engine' that handles all of those changes and transforms the data to pass just a subset to each field and widget so it only sees the data it is concerned with. As a result, your custom widget can't cleanly maintain its own state and can only use theonChange
handlers provided via props to change the formData.
Hey. Thanks for taking the time.
I get that we only get a subset of the formData object and that we can only make changes to that subset using the onChange callback. What i dont get is why also calling my own callback, which updates state in the parent (the same component that renders Form), would prevent this from working. Do you understand what i mean?
Like, why is my setFile call making props.onChange stop working, but if i remove that call or do it within flushSync/setTimeout (schedule it after a rerender) it works.
@nickgros
I dug into this a bit more. In Form
and UNSAFE_componentWillReceiveProps
, nextProps.formData
will still reference the old formData
(since parent has not updated yet) - this is because in onChange
we don't immediately call the onChange
sent in via props, but instead wait for the state to be updated first.
If i instead call this.props.onChange(state)
directly (not in the callback of this.setState
) the parent is updated with the new state in the same batch as setFile
is done and then everything works as expected. When i submit the form it contains the file name and the file
is also set.
So, to clarify:
- We get a
formState
withfile: "some.png"
inonChange
(from user input) - We update state using
this.setState(...)
, but only callthis.props.onChange
as a part of the callback (once state has actually been updated), not immediately - This is then async and in the meantime, since we've called
setFile
(updated state), our parent will re-render andUNSAFE_componentWillReceiveProps
will get run with the same props as before (not updated withformData.file
!!) and this will schedule yet another update where the state is what it was before (not containingformData.file
)
Also: not using controlled Form (setting formData and passing onChange) also causes the entire form to be emptied completely if a parent state change happens in one of the widget callbacks. Did not look into if its even possible to work around this.
I believe, in the end, that these issues boil down to using UNSAFE_componentWillReceiveProps
to create derived state and setState
which relies on this.state
instead of its callback form (this.setState(prevState => nextStateBasedOnPrevState(prevState))
)
Here is just a proof of concept implementation i made with expected behavior https://codesandbox.io/s/compassionate-heyrovsky-dsr62z?file=/src/App.js
Here I don't derive any state but instead I have a variable which either points to valuesFromProps
(formData) or state
(internal state)
So if the component is uncontrolled (no values/formData passed to the component), we use internal state. If the component is controlled (values and onChange passed), we don't derive but simply use the props.
You can remove values
and onChange
of <Form />
in the App
component to see same behavior with uncontrolled Form. Check console and you will see it works as expected. I don't know if not using derived state is feasible for rjsf at this point, but i believe it makes things simpler.
@linde12 Is this still a problem since the replacement of the UNSAFE_componentWillReceiveProps()
with getSnapshotBeforeUpdate()
?