1

In my camera component, I want to upload the photos to a storage bucket every 3 photos. I use state in react to save my image blobs to an array. Everything works perfectly for the first 3 photos but afterward, I am not able to reset my array to be empty again and a seemingly random number of photos is uploaded to my bucket.

  let [blobs, setBlobs] = useState([]);

  const capturePhoto = async () => {
    const photo = await camera.current.takePhoto();
    fetch(photo.path)
      .then(res => {
        setBlobs([...blobs, res._bodyBlob]);
        console.log('blobs', blobs.length, blobs);
      })
      .catch(err => {
        console.log('err', err);
      });
    checkLength();
  };

  const checkLength = async () => {
    if (blobs.length >= 2) {
      // upload files to a folder with the current date in a firebase cloud bucket
      const datestring = new Date().toLocaleString('de-DE');
      blobs.forEach((blob, i) => {
        uploadFile(blob, datestring + '/' + (i + 1) + '.jpg');
      });
      // reset state
      setBlobs([]);
      sendNotification('Photos uploaded');
      toggleDialog();
    }
  };

I console logged my array and the size only increases. Also, it starts console logging with zero although I already added an element likely because setState() is asynchronous. I tried to await the reset by wrapping it in a promise, but that sadly did not work either. How can I upload the blobs to the cloud once there are 3 of them and reset the list afterward?

Felkru
  • 310
  • 2
  • 12
  • Try `setBlobs([]);`, instead of `setBlobs([], sendNotification('Photos uploaded'));` – admcfajn Jul 08 '23 at 23:30
  • 1
    Only class based components use a setState form that accepts a second callback. Also logging right after a setState call will always print the un-updated value, the new value won't be available until the next render cycle. see [The useState set method is not reflecting a change immediately](https://stackoverflow.com/questions/54069253/the-usestate-set-method-is-not-reflecting-a-change-immediately) – pilchard Jul 08 '23 at 23:32
  • @admcfajn I already did. Didn't work sadly. – Felkru Jul 08 '23 at 23:32
  • @pilchard The second callback does work in both cases. – Felkru Jul 08 '23 at 23:34
  • `useState`'s setter function doesn't accept a second callback. Any side-effects should be handled manually in the handler (not relying on updated state values), or in a `useEffect` triggered by the state change. You aren't passing a callback, you are simply calling a function inside another function's arguments – pilchard Jul 08 '23 at 23:36
  • @pilchard so how do I fix the setBlobs([]) then? May I provide you with any additional information? – Felkru Jul 08 '23 at 23:43
  • 2
    It looks like you'll need a `useEffect` dependent on `blobs` that runs your `checkLength` function. Since the `setBlobs` call in that function is wrapped in a condition you won't be at risk of an infinite loop. – pilchard Jul 08 '23 at 23:52
  • The code/description may be incomplete or based on false assumptions, but as pointed out above this is almost certainly a duplicate of [The useState set method is not reflecting a change immediately](https://stackoverflow.com/q/54069253/328193). At the very least, when you call `checkLength()` then it won't have the updated `blobs` from just before calling it. `setBlobs([])` certainly *does* set the state to an empty array, but if you're trying to observe that before the next render then state won't be updated yet. And state updates aren't promise-based, so there's no chance of awaiting them. – David Jul 09 '23 at 00:08

2 Answers2

1

Looks like three things:

  1. The fetch call is not awaited, checkLength is called before the fetch completes.
  2. You don't get the new value of setState until the next render. This is a fundamental idea of React (debatable whether it's a good idea), state values are immutable during a render. setState just gives the next immutable state that will be used by the next render.
  3. When setState depends on the previous state, you should pass a callback to setState instead of using the current value directly. As an example, say you have an empty array, you call fetch once, then again before the first one completes. Both of these setState calls would be referencing the empty array when doing ...blobs. By passing a callback, setState gets the most recent value passed in as a parameter. More info: https://react.dev/reference/react/Component#setstate

Easiest solution is to pass the array as a parameter to checkLength inside of the setState callback.

Here's with .then() as in the question:

  const capturePhoto = async () => {
    const photo = await camera.current.takePhoto();
    fetch(photo.path)
      .then(res => {
        setBlobs(prev => {
          const newBlobs = [...prev, res._bodyBlob];
          console.log('blobs', newBlobs.length, newBlobs);
          checkLength(newBlobs);
          return newBlobs;
        });
      })
      .catch(err => {
        console.log('err', err);
      });
  };

and here's with async await

  const capturePhoto = async () => {
    const photo = await camera.current.takePhoto();
    const res = await fetch(photo.path).catch(console.error);
    if (!res) return;
    setBlobs(prev => {
      const newBlobs = [...prev, res._bodyBlob];
      console.log('blobs', newBlobs.length, newBlobs);
      checkLength(newBlobs);
      return newBlobs;
    });
  };

checkLength

  const checkLength = async (newBlobs) => {
    if (newBlobs.length >= 2) {
      // upload files to a folder with the current date in a firebase cloud bucket
      const datestring = new Date().toLocaleString('de-DE');
      newBlobs.forEach((blob, i) => {
        uploadFile(blob, datestring + '/' + (i + 1) + '.jpg');
      });
      // reset state
      setBlobs([]);
      sendNotification('Photos uploaded');
      toggleDialog();
    }
  };
Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • So the reason was that I only executed my if Statement on the next render? – Felkru Jul 09 '23 at 07:34
  • @Felkru not quite, no. The if statement was executing before the code housed in `.then()`. That was only issue 1. Issue 2 was that you were not referencing the most recent value of the array when copying it for `setState`. Issue 3 was that you were not referencing the most recent value of the array in your if statement. – Chris Hamilton Jul 09 '23 at 12:48
1

So it does come down to how your code executes, specifically the asyncronous nature of setState so what you can do is use the callback form of setState. Here is an example:

setBlobs(prevBlobs => [...prevBlobs, res._bodyBlob]);

Here's a complete example with the rest of the code:

const capturePhoto = async () => {
  const photo = await camera.current.takePhoto();
  fetch(photo.path)
    .then(res => {
      setBlobs(prevBlobs => [...prevBlobs, res._bodyBlob]);
      console.log('blobs', blobs.length, blobs);
    })
    .catch(err => {
      console.log('err', err);
    });
  checkLength();
};

const checkLength = async () => {
  if (blobs.length >= 2) {
    // upload files to a folder with the current date in a firebase cloud bucket
    const datestring = new Date().toLocaleString('de-DE');
    blobs.forEach((blob, i) => {
      uploadFile(blob, datestring + '/' + (i + 1) + '.jpg');
    });
    // reset state
    setBlobs([]);
    sendNotification('Photos uploaded');
    toggleDialog();
  }
};

aleksandar
  • 186
  • 12