Search code examples
javascriptreactjsreact-nativeasynchronoussetstate

Why can't I reset my array with setState?


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?


Solution

  • 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();
        }
      };