brandonw
brandonw

Reputation: 78

Using React Hooks - State not updating

I'm trying show a list of courses with a thumbnail for each course.

The thumbnails are streamed back to browser via an imageService. The imageService returns a Blob to the browser & Url for the image.

const [courses, setCourses] = useState([]);

useEffect(() => {
    courseService
      .getCourses()
      .then((courses) => {
        let updatedCourses = [];        
        courses
          .forEach((course) => {
            if (course.enrolledCourse.imageThumbnailFileName) {
            imgageService
                .downloadIMG(
                  course.imageThumbnailFileName
                )
                .then((data) => {
                  if (data.url) {
                    course.imageThumbnailURL = data.url;                                          
                  }  
                })
            }
            updatedCourses.push(course)
          })
          console.log(updatedCourses) 
          setCourses(updatedCourses);        
      })     
  }, []);

Explaining the intended logic:

  1. Course service returns a list of courses
  2. Image service returns a Blob and URL for course thumbnail for each course to display in a list
  3. Each course is updated with the thumbnail image URL.
  4. setCourses to set the state to the updated courses

The state (courses) does not update with the thumbnail URLs just the original unchanged course list. However, the console.log(updatedCourses) shows the URLs have been added into the array before the setCourses(updatedCourses).

How do I get the updated Courses (with added URLs) into my state object SetCourses? Appreciate any assistance.

UPDATE - trying to use Promises to resolve my issue this is my attempt so far.. What am doing wrong ..promises are not my strong point. enrolledCourses state is still not setting. Any ideas??

UPDATE 2 SOLVED I have posted the working code as an answer below. I've also used the answer from this question's Answer from another question as the basis of my response...

'''

    const [enrolledCourses, setEnrolledCourses] = useState([]);

    useEffect(() => {
     async function getThumbnails() {
     try {
      var collectionArray = [];
      studentService.getStudentEnrolledCourseList(user.id).then((courses) => {
        courses.enrolledCourses.forEach((course) => {
          let withId = {
            docId: course.id,
            course,
          };

          collectionArray.push(withId);

          imageService
            .downloadIMG(
              course.id,
              course.enrolledCourse.imageThumbnailFileName,
              match.params
            )
            .then((download) => {
              withId = {
                ...withId,
                imageThumbnailUrl: download.url,
              };

              collectionArray.push(withId);
            });
        });

        const getImageThumbnailUrl = (course) =>
          studentService.downloadIMG(
            course.id,
            course.enrolledCourse.imageThumbnailFileName,
            match.params
          );

        const addThumbnailUrl = (course) =>
          getImageThumbnailUrl(course).then((imageThumbnailUrl) => ({
            ...course,
            imageThumbnailUrl,
          }));

        Promise.all(collectionArray.map(addThumbnailUrl)).then((data) =>
          setEnrolledCourses(data)
        );
      });
    } catch (e) {
      console.log(e);
    }
  }
  getThumbnails();
}, []);

Upvotes: 0

Views: 110

Answers (3)

brandonw
brandonw

Reputation: 78

Here's what I got working...

const [enrolledCourses, setEnrolledCourses] = useState([]);

useEffect(() => {
  accountService.getById(user.id).then((data) => setStudent(data));

  async function getThumbnails() {
    try {
      var collectionArray = [];

      studentService.getStudentEnrolledCourseList(user.id).then((courses) => {
        courses.enrolledCourses.forEach((doc) => {
          let withId = {
            ...doc,
          };

          collectionArray.push(withId);

          studentService
            .downloadIMG(
              doc.id,
              doc.enrolledCourse.imageThumbnailFileName,
              match.params
            )
            .then((download) => {
              withId = {
                ...withId,
                imageThumbnailUrl: download.url,
              };
              console.log(withId);
              collectionArray.push(withId);
            });
        });

        const getImageThumbnailUrl = (doc) =>
          imageService.downloadIMG(
            doc.id,
            doc.enrolledCourse.imageThumbnailFileName,
            match.params
          );

        const addThumbnailUrl = (doc) =>
          getImageThumbnailUrl(doc).then((imageThumbnailUrl) => ({
            ...doc,
            imageThumbnailUrl: imageThumbnailUrl.url,
          }));

        // console.log(collectionArray)
        Promise.all(collectionArray.map(addThumbnailUrl)).then((data) => {
          console.log("PROMISE FINISHED");
          console.log(data);
          setEnrolledCourses(data);
        });
      });
    } catch (e) {
      console.log(e);
    }
  }
  getThumbnails(); 
}, []);

Upvotes: 0

Thai Duong Tran
Thai Duong Tran

Reputation: 2522

To be able to fix your problem, you need to understand that in Javascript, a lot of tasks, such as fetching data from an external sources (which is what you are doing in your code), are run asynchronously, in a non-blocking manner. It's done that way to avoid making the browser unresponsive.

courses.forEach((course) => {
  if (course.enrolledCourse.imageThumbnailFileName) {
    imgageService.downloadIMG(course.imageThumbnailFileName).then((data) => {
      if (data.url) {
        course.imageThumbnailURL = data.url;
      }
    });
  }
  updatedCourses.push(course);
});
console.log(updatedCourses);
setCourses(updatedCourses);

The problem with the code above is imageService.downloadIMG is an asynchronous function, and Javascript will not wait for it, or the for loop to finish before running the next command, results in your updatedCourses and setCourses function will be run before the images are downloaded.

To fix this problem require quite a solid understanding of asynchronous and promise and I do encourage you to learn more about it. It's essential for any Javascript development.

I will give you an example of how I should write this function with Promise and then syntax. You will have a more readable version if you use async / await.

// Use Promise.all to make the whole loop asynchronous
// what we want to achieve here is to have it (the promise all function) to return 
// a list of courses having image data populated
Promise.all(
  courses.map(
    (course) =>
      new Promise((reject, resolve) => {
        if (course.enrolledCourse.imageThumbnailFileName) {
          return imgageService
            .downloadIMG(course.imageThumbnailFileName)
            .then((data) => {
              if (data.url) {
                course.imageThumbnailURL = data.url;
              }

              // Use the resolve function to tell Javascript the promise is fulfilled and its result
              // which is the course, can be returned
              resolve(course);
            });
        }

        // Resolve the promise immediately if don't have to download the images
        resolve(course);
      }),
  ),
).then((courses) => {
  // The list of courses with images populated should be returned here
  console.log(updatedCourses);
  setCourses(updatedCourses);
});

Upvotes: 1

user7552123
user7552123

Reputation:

If I am not wrong, you have not initialized the state hook correctly. While using Hooks to initialize a state you must declare the initialState via the useState() method

const [courses, setCourses] = useState([]) // your initial state

Hope this helps you

Ref reactjs.org

Upvotes: 1

Related Questions