Updating on custom renderer load

We have several custom renderers that implement some logic to set defaults if the data isn’t already set. To do this we calculate the default and then call handleChange(path, defaultValue) in a useEffect. We are finding that the onChange for the overall JsonForms component isn’t firing and our data isn’t being set in the parent object.

Basic example (using React JsonForm components):

useEffect(() => {
    if (!data) {
      handleChange(path, getDefaultValue());
    }
  }, []);

If I wrap the handleChange in a setTimeout(…, 100), then the onChange for the JsonForms component does fire and we get the updated data.

Is there a better pattern to use to allow a custom renderer to manage setting default data, and proprogating that back to the data object? Using the setTimeout seems like it could be error prone if the timing changes of fully loading the JsonForms component, and we don’t want to set too high of a delay because then the value flickers in the forms.

Other renderers tried a slightly more complicated approached with a local value getting set, and handleChange being called when that value changes, but we are experiencing a similar issue where the default value gets lost, even though handleChange is being called with that value.

1 Like

Hi @Turntwo ,

I tried this in the JsonForms react seed’s RatingControl. Here the data got changed as expected.

Which version of JsonForms are you using? And are you maybe updating JsonForms’ data from the outside shortly after rendering?

Best regards,
Lucas

We are using version 3.0.0 of JsonForms.

We have the json data in a context, and the context is getting updated in the JsonForm onChange event.
When we call handleChange in a custom renderer (which usually triggers the onChange on the JsonForms component), the onChange isn’t firing unless we put it in a setTimeout. The handleChange works fine (onChange gets triggered) for updates from actual user interactions.

It seems like the renderers are being invoked before JsonForms is fully initialized and connected to the onChange event handler, so those initial handleChange calls are being lost or overriden.

I see. Please try upgrading to the latest version 3.2.1. Version 3.0.0 was released a long time ago and this might just work with the latest version. If it still doesn’t work then, we can investigate further.

I updated to 3.2.1 and the problem is actually worse now. The previous issue that I fixed with a setTimeout is now back, and I’m getting stale data when the form loads.

Hi!

Handling the defaults within the render path with useEffect is not a good pattern in general as you are basically placing logic into the UI rendering, additionally triggering a lot of rerenderings.

Still I’m not sure how the describes issues arise as JSON Forms maintains its internal state and calling handleChange arbitrarily often and at arbitrary times should not lose any data at any point, as in the end all changes run sequentially through our core reducer. Having said that, you probably have many renderers using that pattern, triggering many rerenderings at the same time, so I could be overlooking something.

I would like to recommend to handle the default values either:

  1. outside of JSON Forms by properly initializing your data before handing it over, or
  2. use our new middleware functionality to inject the default initialization into the state handling of JSON Forms

For option 1: If you handle arbitrary data and don’t want to implement all the default calculation yourself, you can use AJV’s default support. Just create an AJV instance, validate the data once, and all defaults will be in there. If you want to use that also during the lifetime of the form, hand over the AJV instance to JSON Forms, see here for the docs.

For option 2: The documentation for the middleware is not merged yet, however here you can find the current preview state. You can basically inject after our INIT and UPDATE_DATA actions and modify the form-wide state to your liking. Use this approach only if you want to have ongoing-default support which is not covered by AJV.

Our approach seems to have run into several issues, so I have already been looking into the options you recommended. Will most likely migrate to using the middleware, since I think that will meet our requirements.

The defaults seem to be fixed by setting a timeout for those cases where the onChange isn’t getting triggered after calling handleChange. We have some other data relationships that get set when a user updates different properties, managed by a wrapper renderer through useEffects, which work after the initial load but are causing re-rendering on the initial load and onChange is getting called with stale data (doesn’t seem to be sequential, which I’m guessing has to do with React’s new concurrent rendering or something they are doing differently because it worked fine on React 16). I’m hopeful the middleware options will simplify this by consolidating the logic and triggering it on each state update before passing it back to original data source.

I was trying to get a quicker fix in (similar to the setTimeout fix for the defaults) for these other issues, but it is only working in some cases. Migrating to the middleware option is a bigger change, so was hoping to save that for farther down the line.

Is there any way to tell when JsonForms has been fully loaded/rendered? There didn’t seem to be a loaded or ready event to tie into.

Hi @Turntwo,

JSON Forms is essentially a regular React component and we render within one render pass. When custom renderers arbitrarily trigger data changes via useEffect then this is nothing what JSON Forms can possibly know about, therefore there is no “fully loaded / rendered” event you can listen to.

As the additional rendering/data loading is triggered by the custom renderers you could build such an event callback yourself, for example via an own React context in which all additional data handling is tracked. This might help you in the short run / workaround the issue. However it’s digging even deeper into an overall unhealthy pattern of coupling UI with business logic.

The code posted above shows a one-time data initialization pattern in the custom renderer. In case your default values are static, then it’s very straightforward to switch to an AJV-based default initialization, as all you need to do is to specify a default value for all properties in your JSON Schema, evaluate the data one time and all default values are set. So if that is the use case at hand, then I would recommend to use that approach directly.

If you have more complex/dynamic use cases you will need to evaluate how easy it is for you to switch to them.

We do use ajv for static default values, but had a use case for configurable variable defaults (e.g. dates that could default to today, yesterday, etc) that we wanted to be able to specify the type of default in the schema (rather than in a context), so we could reuse that defaulting logic across forms with different contexts. The setTimeout actually seems to work fairly well for those use cases (although I’m not entirely comfortable with that as a solution since it becomes timing dependent). Moving that logic out of the renderer and into the context was a solution, but then we can’t specify a default in the schema (well, I guess we could process the schema on our own to determine options and manage that in the context instead of within JsonForms, just seemed like JsonForms was already parsing that so better to localize that and not duplicate parsing of the schemas).

The trickier issue is when we want to update the data object based on several business rules that should be applied as the data is changed by a user, but also on initial load. We had placed this into a wrapper renderer so we could use the uischema to specify when to use different logic classes depending on use case, however with the React update the useEffects seem to run in parallel and state updates are getting overwritten with later updates (even when using functional setState calls). This works fine during user interaction, but the various updates conflict on the initial load. Spreading the old jsonData with the updates in onChange when setting the context jsonData helped with losing data, but also created a render loop.

I’m going to attempt to move this logic into a middleware function or into the wrapping context - I’m not sure how that will interaction with JsonForm will behave, since I think the middleware will still trigger the onChange, which updates the context data and if those updates happen in parallel we might have a similar problem. You said that JsonForms all renders within one pass, but since we upgraded to React 18 we seem to be getting overlapping updates. All this functionality was behaving mostly as expected prior to the React upgrade.

Hi @Turntwo,

We built something similar with a pretty complex “rule-parsing-and-apply” engine which can perform arbitrary data manipulation based on declarative rules. However we integrated it outside of the UI. The engine runs on every data change and the modified data is fed back to JSON Forms.
Besides needing to solve additional complexity (e.g. what shall happen if the user modifies the data even further before the engine completes) this works fine, especially so with the middleware as it can be much better extracted what the user actually changed.

React has a lot of performance improvements with the later versions where updates are batched etc. Something there seems to go wrong for you, as the update mechanism is tied to the rendering via useEffect.

Maybe you are also running into issues with the controlled vs uncontrolled behavior of JSON Forms. Can you try what I suggested here to see whether it then works better for you?

We’ve managed to get things working pretty well again (one small issue came up that still needs to be addressed) - and I think we will be able to move the defaults into the middleware as well (the original question for this post, currently still using setTimeouts for that behavior). We used a combination of techniques to clean up the re-rendering issues/flickering/looping.

  • Moved most of the data relation updates into middleware.
  • Ensured Custom Renderers manipulate the data state through handleChange calls (some renderers were updating the context state within useEffects, which resulted in updates from stale data that overwrote data updated through handleChange/middleware. This is needed for cases where the data updates are dependent upon local renderer state (such as a toggle for different views) and not just on other data properties.
  • Context data state is only updated through the JsonForms onChange event.

I did consider moving to an uncontrolled JsonForms component, but I don’t think that is needed at this time and would introduce other issues that needed to be handled.

Thanks for the help.

1 Like