Search code examples
reactjsasynchronoususe-effect

How can I asynchronously update multiple objects in the same array in React?


I have an application with a class as defined below:

class Point {
  id: string;
  position: Coordinate;
  elevation?: number;
}

In my application I store an array of points.

const [points, setPoints] = useState<Point[]>([]);

In the code below, I have a useEffect hook that depends on the points array; any time points changes, the effect scans for points with undefined elevation and queries asynchronously for the elevation. When individual queries return, a callback handleUpdatePoint is called to update that particular Point with a new elevation value.

  // Any time the Points array changes, checks each Point for undefined elevation
  // and queries for it
  //  ISSUE: If more than one elevation query has yet to resolve, there will be an unintended
  //         state if one query resolves while one or more queries are pending
  useEffect(() => {
    points.forEach((p) => {
      if (p.elevation === undefined) {
        ElevationQuery(p.position.lat, p.position.lng, (elevation) => {
          handleUpdatePoint(p.id, { elevation: elevation });
        });
      }
    });
  }, [handleUpdateSteerpoint, points]);`

The issue I'm facing is that if multiple queries are pending at the same time, the state of points is overwritten as multiple results return, thus losing elevation results for some of my queries. How can I fix this?


EDIT #1: handleUpdatePoint added for reference

const handleUpdatePoint = useCallback(
    (id: string, newPointData: Partial<Point>) => {
      // shallow copy the points array
      const pointsClone = [...points];

      // the point we are updating
      const targetPoint = getPointById(id);

      // preserve all other points as they are, but update the target point
      // with the new data
      const updatedPoints: Point[] = pointsClone.map((p: Point) => {
        if (p.id !== id) {
          return p;
        } else {
          // if the target point has a position, change its elevation to undefined
          if (newPointData.position) {
            newPointData.elevation = undefined;
          }

          const updatedPoint: Point = update(targetPoint, { $merge: newPointData })!;
          return updatedPoint;
        }
      });

      setPoints(updatedPoints);
    },
    [getPointById, points]
  );

Solution

  • just using a callback-style of updating your state would solve the issue:

    setPoints((previousPoints) =>
      previousPoints.map((p: Point) => {
        if (p.id !== id) {
          return p;
        } else {
          // if the target point has a position, change its elevation to undefined
          if (newPointData.position) {
            newPointData.elevation = undefined;
          }
    
          const updatedPoint: Point = update(targetPoint, {
            $merge: newPointData,
          })!;
          return updatedPoint;
        }
      })
    );