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

The new React lifecycle methods - breaking change

Open magaton opened this issue 1 year ago • 6 comments

Prerequisites

What theme are you using?

mui

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

Up until version 5.13.2 our app, based on rjsf (mui) was working well. Exceptions are the issues I have already filed:

But with version 5.13.3 in Form.tsx UNSAFE_componentWillReceiveProps was replaced with: componentDidUpdate and getSnapshotBeforeUpdate.

Here is the gist of the problem. We have 3 screen admin UI, each with its own schema and UI schema. We also have a couple of custom select widgets that take a value from formData, do some calculations, and update the state (formData) with the result array. The issue happens on the initial render of, e.g. 2nd screen, where based on the formData from the 1st screen we need to set defaults (update formData). With UNSAFE_componentWillReceiveProps, it was working well, namely when our custom selects fired, the change was detected in UNSAFE_componentWillReceiveProps and the screen was re-rendered, but now, componentDidUpdate is not called on the initial render (as described in jsdocs and react docs).

I understand that the main use case supported by the current design is to change state based on onChange property in the <Form>, but in our case (initial render), there is no onChange event. We read passed formData in the custom widget, do the calculation, and update the state that is then used for conditional schemas on the 2nd screen.

What would be the recommended way to force re-render in our custom select widget, now that there is no UNSAFE_componentWillReceiveProps?

We feel stuck :( I filed this issue as a feature since it is related to our multi-form scenario with custom widgets, so not really a common use case. Sorry, if not appropriate.

Thanks very much in advance!

Describe the solution you'd like

Any idea/workaround/feature-switch that would allow re-rendering from our custom widgets.

Describe alternatives you've considered

We are not a react shop, so most of our investigation was chasing our own tail. We have tried:

  • adding static getDerivedState, no luck
  • force props.onChange in our custom select which seems to help, but this results with a React warning and smells badly
  • detect the initial render with useRef - it does help when our custom select works with the previous formData, but we also have a custom select that needs both previous formData and specific fields on the 2nd form filled (so it is not just initial render, that we need to handle).

magaton avatar Nov 13 '23 20:11 magaton

@magaton Odd. I would have expected that the constructor for Form would have determined that the formData updated on the initial state setup and called onChange. Can you debug and let us know whether this is the case? And if so, then that means we likely have a bug in our constructors.

heath-freenome avatar Nov 17 '23 20:11 heath-freenome

No, the form does not see the change if the state is changed in the custom widget during the initial render. I am not good with React so I can't really tell what the root cause is, but I created a project that demonstrates the problem.

The app is using the latest 5.14.2 and is not working properly. The same is with all versions since 5.13.3.

There is also a README summary about my observations and the screenshot of how it looks with version 5.13.2 (before lifecycle methods change). If you change @rjsf version to 5.13.2 you will be able to verify this

Thanks again for looking into this.

magaton avatar Nov 18 '23 19:11 magaton

@magaton in both widget components, instead of modifying formData directly and then doing a shallow copy ({ ...formData }), it is recommended to create a completely new state object. This ensures that React detects the state change and re-renders the component.

In all places code snippet:

let newFormData = formContext.formData;

replace with code:

let newFormData = { ...formContext.formData };

It worked for me.

lrozewicz avatar Nov 20 '23 18:11 lrozewicz

@lrozewicz Thanks very much for the reply. It took me basically a whole week to test it. I have applied the change you advised; it does solve one problem but creates others.

There is definitely a different behavior noticed in both 5.13.2 and 5.14.2 versions of @rjsf with the:

let newFormData = { ...formContext.formData };

instead of:

let newFormData = formContext.formData;

TBH, although I understand what you are saying, the last step I am doing is that I call

formContext.setFormData({...newFormData});

which should recreate the object and make the change noticed in @rjsf library.

Some of the behaviours are quite odd, like that:

  • it works for one field, but not the other, if two fields are together in the schema.
  • the formData console log seems accurate but the condition is not reevaluated for the dependant schema when field that drives the condition gets updated. Then if I only touch the schema and app is reloaded, everything is ok...

I feel I cannot find my way through since there is not a single:

  • way how the form updates are handled in my custom widget
  • @rjsf version that is fully working with my schema.

Then there is a question of defaults, how they are initially populated in formData depending on:

  • version (5.13.2 vs 5.14.2)
  • the existence of my custom widgets (the order how they are invoked with regards to initial render (with the defaults)
  • the way how form is updated in my custom widgets

@heath-freenome I am now questioning whether there is something in my custom widgets that is causing this malfunction. And I am slowly getting crazy about the whole gig since I am running in circles without proper understanding

Is there an example (sample project) that would demonstrate the following concepts I am employing in my project, probably in the wrong way

  • MyFormComonent where I load initially (passed) formData, register my custom widgets in the registry and implement onChange handler

  • Custom Select Widget that reads formData field(s), caclulates a field (array), populates options in the select widget, and sets the array field in the formData. In other words: 1 or more useEffects vs useMemo.

Thanks, Milan

magaton avatar Nov 24 '23 11:11 magaton

@magaton WOW, you are really bending the heck out of the rjsf code base. I'm curious why you aren't doing most of this work inside of onChange() handlers and passing up the local sub-formData objects to the parent so that the default processing logic of RJSF works for you rather than changing the whole of the form-data out of phase in useEffects()?

heath-freenome avatar Dec 01 '23 21:12 heath-freenome

I wish I knew myself :) but, this is the 1st React code I have seen in my life :) I was always on the other side :) Sorry, but I do not understand what you are suggesting. Would you mind explaining it a bit?

magaton avatar Dec 01 '23 22:12 magaton