Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

props.onChange ignored if i update other state in the parent component from a custom widget #3914

Open
4 tasks done
linde12 opened this issue Oct 20, 2023 · 6 comments
Open
4 tasks done

Comments

@linde12
Copy link

linde12 commented Oct 20, 2023

Prerequisites

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

  1. Go to https://codesandbox.io/s/wonderful-wave-sp997w?file=/src/App.js
  2. See console, formData and file are printed periodically
  3. Choose a file
  4. Verify that formData contains a file path (fakepath) and file is a reference to a File
  5. Comment out the flushSync line and uncomment the the line that does setFile(file) without flushSync
  6. Reload the page and do step 2, 3 and 4 again.
  7. 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 linde12 added bug needs triage Initial label given, to be assigned correct labels and assigned labels Oct 20, 2023
@nickgros
Copy link
Contributor

@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.

@nickgros nickgros added awaiting response and removed bug needs triage Initial label given, to be assigned correct labels and assigned labels Oct 20, 2023
@linde12
Copy link
Author

linde12 commented Oct 20, 2023

@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.

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.

@linde12
Copy link
Author

linde12 commented Oct 23, 2023

@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:

  1. We get a formState with file: "some.png" in onChange (from user input)
  2. We update state using this.setState(...), but only call this.props.onChange as a part of the callback (once state has actually been updated), not immediately
  3. This is then async and in the meantime, since we've called setFile (updated state), our parent will re-render and UNSAFE_componentWillReceiveProps will get run with the same props as before (not updated with formData.file !!) and this will schedule yet another update where the state is what it was before (not containing formData.file)

@linde12
Copy link
Author

linde12 commented Oct 23, 2023

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)))

@linde12
Copy link
Author

linde12 commented Oct 23, 2023

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.

@heath-freenome
Copy link
Member

@linde12 Is this still a problem since the replacement of the UNSAFE_componentWillReceiveProps() with getSnapshotBeforeUpdate()?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants