Reputation: 78
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:
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
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
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
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
Upvotes: 1