0

In my React application, I have an array of objects implemented using a "useState" hook. The idea is to update an existing object within this array if an object with a specific "id" already exists or to add a new object if it does not.

The problem that I am encountering is that my state is not up-to-date before I am modifying it (= updating and/or adding objects). Therefore, the check whether an object with a certain "id" already exists fails.

Here is a (very much simplified) example demonstrating the problem:

import React, { useEffect, useState } from 'react';

interface Dummy {
  id: string;
  value: number;
}

const TestComponent = () => {
  const [dummies, setDummies] = useState<Dummy[]>([]);

  const addDummy = (id: string, value: number) => {
    const dummyWithIdAlreadyUsed = dummies.find(
      (dummy: Dummy) => dummy.id === id
    );
    if (!dummyWithIdAlreadyUsed) {
      setDummies((dummies) => [...dummies, { id: id, value: value }]);
    }
  };

  const test = () => {
    addDummy('dummy1', 1);
    addDummy('dummy1', 2);
    addDummy('dummy2', 3);
    addDummy('dummy2', 4);
    addDummy('dummy3', 5);
    addDummy('dummy4', 6);
    addDummy('dummy4', 7);
  };

  useEffect(() => {
    console.log(dummies);
  }, [dummies]);

  return <button onClick={test}>Test!</button>;
};

export default TestComponent;

The desired output is:

0: Object { id: "dummy1", value: 1 }
1: Object { id: "dummy2", value: 3 }
2: Object { id: "dummy3", value: 5 }
3: Object { id: "dummy4", value: 6 }

The actual output is:

0: Object { id: "dummy1", value: 1 }
1: Object { id: "dummy1", value: 2 }
2: Object { id: "dummy2", value: 3 }
3: Object { id: "dummy2", value: 4 }
4: Object { id: "dummy3", value: 5 }
5: Object { id: "dummy4", value: 6 }
6: Object { id: "dummy4", value: 7 }

How can I make sure that my state is up-to-date before executing setDummies?

bassman21
  • 320
  • 2
  • 11
  • 1
    You're checking the wrong version of `dummies` (reusing the name likely hasn't helped), the old one you're closed over not the new one passed to the [functional update](https://reactjs.org/docs/hooks-reference.html#functional-updates). – jonrsharpe Aug 29 '22 at 19:40
  • I am sorry but could you please elaborate further? I am not sure that I am getting yor point. Also, this is just a simplified example that I created to demonstrate the problem I was encountering in a "real" React application where my objects obviously are not called "dummies". – bassman21 Aug 29 '22 at 19:45
  • Does this answer your question? [Why does calling react setState method not mutate the state immediately?](https://stackoverflow.com/questions/30782948/why-does-calling-react-setstate-method-not-mutate-the-state-immediately) – Konrad Aug 29 '22 at 19:47
  • It's not the specific name that's the problem, but the shadowing. Think about what a functional update does and why you needed it in the first place. – jonrsharpe Aug 29 '22 at 19:51
  • (Or if you just cargo-culted a functional update, which would explain why you're asking a question about a problem solved by a feature you're already using, read the linked docs to get a clearer idea of what's going on.) – jonrsharpe Aug 29 '22 at 19:59

2 Answers2

2

State updates are asynchronous and queued/batched. What that means here specifically is that the dummies variable (the one declared at the component level anyway) won't have an updated state until after all 7 of these operations have completed. So that if condition is only ever checking the initial value of dummies, not the ongoing updates.

But you do have access to the ongoing updates within the callback here:

setDummies((dummies) => [...dummies, { id: id, value: value }]);

So you can expand that callback to perform the logic you're looking for:

const addDummy = (id: string, value: number) => {
  setDummies((dummies) => {
    const dummyWithIdAlreadyUsed = dummies.find(
      (dummy: Dummy) => dummy.id === id
    );

    if (!dummyWithIdAlreadyUsed) {
      return [...dummies, { id: id, value: value }];
    } else {
      return dummies;
    }
  });
};

In this case within the state setter callback itself you're determining if the most up-to-date version of state (as part of the queue of batched state updates) contains the value you're looking for. If it doesn't, return the new state with the new element added. If it does, return the current state unmodified.

David
  • 208,112
  • 36
  • 198
  • 279
-1

This is a problem of closure, you can fix it by using an arrow function in the setter, check this article : https://typeofnan.dev/why-you-cant-setstate-multiple-times-in-a-row/

Maybe you can refactor your code to avoid calling setters many times in a row by doing javascript operation before calling the setter

for exemple :

const test = (data: Dummy[]) => {

// to remove duplicate and keep the latest
const arr = data.reverse()

const latestData = arr.filter(
(item, index) => 
arr.indexOf(item) === index
);

setDummies([...dummies, ...latestDummiesData])
};

H-DC
  • 92
  • 2