Efficient State Management for Concurrent API Calls in React

Hello Team,

I am currently facing a challenge with managing state updates in a React component where multiple concurrent API calls are involved. Specifically, I am using the useState hook to handle formdata and updating this state based on the results from several parallel API requests. Here is a summary of my approach:

import { JsonForms } from "@jsonforms/react";
import {
  materialRenderers,
  materialCells,
} from "@jsonforms/material-renderers";
import { Button } from "@mui/material";
import { useState } from "react";
import schema from "./schema.json";
import "./styles.css";
import uischema from "./uischema.json";

export default function App() {
  const [formdata, setFormdata] = useState({});
  const [errors, setErrors] = useState([]);
  const clickHandler = () => {
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name1: json.value,
        }));
      });
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name2: json.value,
        }));
      });
    fetch("https://mocki.io/v1/3040136a-0ecc-477b-8251-7f4984508a46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name3: json.value,
        }));
      });
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name4: json.value,
        }));
      });
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name5: json.value,
        }));
      });

    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name6: json.value,
        }));
      });
    fetch("https://mocki.io/v1/3040136a-0ecc-477b-8251-7f4984508a46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name7: json.value,
        }));
      });
    fetch("https://mocki.io/v1/3040136a-0ecc-477b-8251-7f4984508a46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name8: json.value,
        }));
      });
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name9: json.value,
        }));
      });
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name10: json.value,
        }));
      });
    fetch("https://mocki.io/v1/73bc6c1f-471f-4092-be45-a358321d9c46")
      .then((response) => response.json())
      .then((json) => {
        setFormdata((pre) => ({
          ...pre,
          name11: json.value,
        }));
      });
  };
  return (
    <div>
      <div className="container">
        <JsonForms
          data={formdata}
          renderers={materialRenderers}
          cells={materialCells}
          onChange={({ data, errors }) => {
            setFormdata(data);
            setErrors(errors);
          }}
          schema={schema}
          uischema={uischema}
        />
        <Button
          variant="contained"
          sx={{ marginTop: "10px" }}
          onClick={clickHandler}
        >
          Call API
        </Button>
      </div>
    </div>
  );
}

In this setup, each API request updates a different field within formdata, leading to frequent re-renders of the component and noticeable flickering in the UI.

Note: Due to constraints within our project structure, I am unable to use Promise.all or await/Promise chaining to wait until all API requests resolve. Each API call must update the state independently as soon as it completes.

Question: Is there an efficient method to batch state updates or otherwise optimize the handling of concurrent API responses to minimize UI flickering and excessive re-rendering? I am seeking best practices or alternative approaches that ensure the component updates with each API response while mitigating the issue of frequent re-renders.

Thank you for your assistance.

Hi @SatendraRaghav,

We try to minimize rendering as much as possible. For example all our off-the-shelf renderers are memoized and will not rerender if their props do not change.


These are two different requirements which conflict with each other. Either the state is updated as early as possible or they are batched for increased performance. In case you expect all of the responses to arrive at roughly the same time, then some small window of a few milliseconds to collect the state updates could be useful. However this will make your update logic more complex.


Normally I would expect the code of yours to work fine. Can you elaborate what you experience with “flickering”? I ran your code on my machine and it looked fine.

Note that we have an open issue in which we accidentally rerender arrays too often leading to performance issues, see here. Maybe that is the same issue you are encountering? I can not verify as you did not post your schema and UI schema.
We expect this issue to be fixed with the next version of JSON Forms.

1 Like

Thank you for your response.

You can review the provided CodeSandbox link, where both the schema and UiSchema are implemented. Upon clicking the button that triggers data updates, you will notice that some components fail to populate with the resolved API data. Interestingly, this behavior occurs randomly, with different components affected each time the update is triggered.

In my current project, which involves a more complex schema and custom renderers, this issue manifests as data flickering and inconsistent form updates. As the schema and UI logic become more intricate, these rendering inconsistencies are becoming increasingly problematic.

Hi @SatendraRaghav,

Sadly the CodeSandbox is not accessible. Can you check the settings?

Hi @sdirix ,

Apologies for the earlier settings issue. I have now corrected the privacy settings from private to public and updated the link accordingly.
Updated CodeSandbox Link

Thanks! However in the sandbox I can’t reproduce the issue. All fields fill correctly for me, even when enabling cpu / network throttling.

You could be experiencing a racing condition as we have rapid firing updates from the form as well as from all the outside data updates coming in. These could override each other.

You can get more control over the situation by using a middleware. See here for the documentation.

What you could do is using the middleware to selectively update the form data and to not overwrite it completely. This way, you can’t accidentally override data which was just set from the outside world.

In short:

  • Do not use the onChange handler
  • Instead use a middleware
  • In the middleware you can get the user intended change via:
import setFp from 'lodash/fp/set';

// middleware, within a useMemo
(
    state: JsonFormsCore,
    action: CoreActions,
    defaultReducer: (state: JsonFormsCore, action: CoreActions) => JsonFormsCore
): JsonFormsCore => {
    const newState = defaultReducer(state, action);
    // user triggered change
    if (action.type === 'jsonforms/UPDATE') {
        // the changed data, referred to via the path
        const newData = get(newState.data, action.path);
        // now you have the "newData" and the "path" in hand
        setData(prevData => setFp(action.path, newData, prevData))
        // return the previous state for "controlled" style
        return state;
    }
    return newState;
};

This way you should no longer have any data races, except for fields which are at the very same time edited via your callbacks and from the user. If that is a use case, then you need to decide on how to handle them.

Thank you @sdirix for your prompt response. To replicate the issue, please refresh the application and click the button. After repeating this process 4-5 times, you will observe that occasionally, some fields fail to populate and remain empty, while at other times, all fields fill correctly. For further clarification, I will provide a video demonstrating the issue using the same code from codesandbox.
Video Link

In my case, only the callbacks are modifing form data, and no user action is being performed at that time.