How to access the updated state in the body of a function that has already called setState?

Viewed 53

I'm currently coding a function to save the blogPost object into a document in my Firestore database.

The intended routine should be:

  • Click on SAVE button and call the savePost() function
  • The savePost() function should call the uploadImagesAndGetURL() function because the images of the blogPost should be uploaded to Firebase Storage before I save their src URL to the Firestore. (in my real case the image upload is async and I await for it to complete, but I don't think it hurts the example here).
  • The uploadImagesAndGetURL() should upload the images and store the returned urls for each one in their respective src property, inside the elements array of the state.
  • It does that by running a forEach() on the elements array and calling setState() (setBlogPost) to update state with the new URL's received from the storage.
  • The saveToFirestore() function will then be called and should be able to access the most recent state to be able to save the blogPost object along with the image src's to the database.

QUESTION

My problem is that while I'm running the savePost() function the component is being re-rendered 3 times in this case (for 3 images), because the uploadImagesAndGetURL() is calling setBlogPost() 3 times. One for each image. And when I reach the saveToFirestore() function and I log the blogPost state, you can see that only the first image src has changed.

Am I doing it right? How can I improve this routine to make sure everything has been updated before I saveToFirestore()?

NOTE: One way to do it would be to skip the state update and pass the returned URLs straight to the saveToFirestore() function without ever storing them on the state. But, what is the appropriate pattern in this situation?

Log from the saveToFirestore() function:

{
  "title": "This is the title",
  "body": "Click on the button to upload this post",
  "elements": [
    {
      "type": "image",
      "src": "source0"
    },
    {
      "type": "image",
      "src": null
    },
    {
      "type": "image",
      "src": null
    }
  ]
}

SNIPPET BELOW

function App() {
  console.log('Rendering App...');
  const [blogPost,setBlogPost] = React.useState({
    title: 'This is the title',
    body: 'Click on the button to upload this post',
    elements: [
      { type: 'image',
        src: null 
      },
      { type: 'image',
        src: null
      },
      { type: 'image',
        src: null 
      }
    ]
  });

  function savePost() {
    console.log('Inside savePost');
    uploadImagesAndGetURL();
    saveToFirestore();
  }
  
  function uploadImagesAndGetURL() {
    console.log('Inside uploadImages');
    blogPost.elements.forEach((item,index) => {
      console.log('Inside uploadImages - forEach');
      setBlogPost((prevState)=>{
        const aux = Array.from(prevState.elements);
        aux[index].src = 'source' + index;
        return({
          ...prevState,
          elements: aux
        });
      });
    });
  }
  
  function saveToFirestore() {
    console.log('Inside saveToFirestore');
    console.log('Will save the following blogPost to Firestore');
    console.log(blogPost);
  }

  return(
    <React.Fragment>
      <div>{blogPost.title}</div>
      <div>{blogPost.body}</div>
      <div>Image 1 src: {blogPost.elements[0].src}</div>
      <div>Image 2 src: {blogPost.elements[1].src}</div>
      <div>Image 3 src: {blogPost.elements[2].src}</div>
      <button onClick={savePost}>Save Post</button>
    </React.Fragment>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

1 Answers

You can move the save logic within the useEffect hook as you are changing the blog state, it'd trigger the effect to run.

Follow along on Edit so.answer.56544887

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  console.log("Rendering App...");
  const [blogPost, setBlogPost] = React.useState({
    title: "This is the title",
    body: "Click on the button to upload this post",
    elements: [
      { type: "image", src: null },
      { type: "image", src: null },
      { type: "image", src: null }
    ]
  });

  function savePost() {
    console.log("Inside savePost");
    uploadImagesAndGetURL();
    // saveToFirestore();
  }

  function uploadImagesAndGetURL() {
    console.log("Inside uploadImages");
    const elements = blogPost.elements.map((_, index) => ({
      type: "image",
      src: `source${index}`
    }));
    setBlogPost(previousPost => ({ ...previousPost, elements }));
  }

  // function uploadImagesAndGetURL() {
  //   console.log("Inside uploadImages");
  //   blogPost.elements.forEach((item, index) => {
  //     console.log("Inside uploadImages - forEach");
  //     setBlogPost(prevState => {
  //       const aux = Array.from(prevState.elements);
  //       aux[index].src = "source" + index;
  //       return {
  //         ...prevState,
  //         elements: aux
  //       };
  //     });
  //   });
  // }

  useEffect(() => {
    function saveToFirestore() {
      console.log("Inside saveToFirestore");
      console.log("Will save the following blogPost to Firestore");
      console.log(blogPost);
    }
    saveToFirestore();
  }, [blogPost]);

  return (
    <React.Fragment>
      <div>{blogPost.title}</div>
      <div>{blogPost.body}</div>
      <div>Image 1 src: {blogPost.elements[0].src}</div>
      <div>Image 2 src: {blogPost.elements[1].src}</div>
      <div>Image 3 src: {blogPost.elements[2].src}</div>
      <button onClick={savePost}>Save Post</button>
    </React.Fragment>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

the uploadImagesAndGetURL() is calling setBlogPost() 3 times.

And also I'd actually get the list of URLs and set the elements property in one go to prevent re-render 3 times.

Related