6

I'm trying to update an array from a put request

const [descriptors, setDescriptors] = useState([]);

const handleDescriptorUpdate = (id, descriptorData) => {
    services
      .putDescriptor(id, descriptorData)
      .then((response) => {
        const descriptorIndex = _.findIndex(descriptors, (e) => e.id === id);
        if (descriptorIndex !== -1) {
          const tempDescriptors = [...descriptors];
          tempDescriptors[descriptorIndex] = response.data;
          setDescriptors(tempDescriptors);
        }
      })
      .catch((error) => {
        console.error(error);
      });
  };

This works fine when I perform only 1 request at the time, but when I click the button that performs the update twice the promises, instead of spreading the array and updating the old value with the new one, both of them are spreading the same array causing that when the second promise is resolved, it updates the state with the new value (second value) that came back from the server BUT changing the first value (the one changed by the first promise) for its original value.

Descriptors are initially filled with an array of objects (from a get request):

[
  {
    "id": 24,
    "name": "Test 4.1",
    "percentage": 0,
    "towerId": "5cfc9200-c04a-11e9-89c0-2dd5d3707b1b"
  },
  {
    "id": 23,
    "name": "Test 3.1",
    "percentage": 0,
    "towerId": "5cfc9200-c04a-11e9-89c0-2dd5d3707b1b"
  }
]
  • Welcome to SO! I don't think this is really a [mcve]--is `descriptors` an array of objects? Clarification appreciated--thanks much! – ggorlen Oct 03 '19 at 00:21
  • That's weir because useState doesn't reflect changes immediately, it will be nice if someone knows how to force the useState to be synchronous – DariusV Oct 03 '19 at 00:53

2 Answers2

3

I saw you let descriptors as a state instead of a reference, as I said in my comment above useState doesn't reflect changes immediately so keep on memory one reference of your array, you can do it with the hook useRef, see the next example:

const [descriptors, setDescriptors] = useState([]);

const descriptorsReference = useRef(null);

const handleDescriptorUpdate = (id, descriptorData) => {
    services
      .putDescriptor(id, descriptorData)
      .then((response) => {
        const descriptorIndex = _.findIndex(descriptors, (e) => e.id === id);
        if (descriptorIndex !== -1) {
         // Use descriptorsReference instead
          const tempDescriptors = [...descriptorsReference.current];
          tempDescriptors[descriptorIndex] = response.data;
          // Next line is to update the descriptors into descriptors state, this phase doesn't happen immediately 'cause is asynchronous 
          setDescriptors(tempDescriptors);
          // Next line is to update the descriptors in memory, this phase occurs immediately 
          descriptorsReference.current = tempDescriptors
        }
      })
      .catch((error) => {
        console.error(error);
      });
  };
DariusV
  • 2,645
  • 16
  • 21
  • [This is not a proper way to make use of `ref`](https://reactjs.org/docs/refs-and-the-dom.html). – Joseph D. Oct 03 '19 at 02:31
  • @JosephD. please explain why is it not the right way to use ref in this case instead of letting a link about refs and one down vote – DariusV Oct 03 '19 at 04:15
  • I can solve these storing descriptors with none-state variables in a functional component as the next answer sais, please take a look at this, and apport if you know a better one way: https://stackoverflow.com/a/53146714/6455357 – DariusV Oct 03 '19 at 04:22
  • because it's an anti-pattern, when you already have state hooks for such purpose. Using something because it works simply means [just making things happen](https://reactjs.org/docs/refs-and-the-dom.html#dont-overuse-refs). – Joseph D. Oct 03 '19 at 04:57
1

Even though React batches all setStates done during an event handler.

setDescriptors is outside the scope of the event handler for it's only called when promise is resolved.

Thus, you would need to make use of a state callback to properly manage descriptor versions.

.then((response) => {
  setDescriptors((descriptors) => ( // use state callback
    descriptors.map((desc) =>
      desc.id === id ? { ...desc, ...response.data } : desc
    ) 
  })
}
Joseph D.
  • 11,804
  • 3
  • 34
  • 67
  • This is the same code as the original one but searching for old descriptors, the error occurs because when the promise is called the descriptors are not set yet so the descriptors will not be on the older ones besides how will you know which is the right descriptor, every time the id is the same – DariusV Oct 03 '19 at 02:09
  • @DariusV, as mentioned *"Descriptors are initially filled with an array of objects (from a get request)"*. Also, this code makes use of the previous state descriptors instead of array spreading to make sure any updates are done on the recent version of descriptors. – Joseph D. Oct 03 '19 at 02:29
  • I just tested that code and It doen't solve anything because as I said the descriptor has not set yet when map is executed, the useState updates the state asynchronous – DariusV Oct 03 '19 at 04:09
  • @DariusV give me a link to your codesandbox. The prerequisite for `handleDescriptorUpdate` is `descriptors` have already been initialized via get. The `descriptors` you see inside the state callback is the previous state. – Joseph D. Oct 03 '19 at 04:55