I'm getting images via an API that is fetched in a useEffect
hook.
I want to store a random image into an object array and display it immediately when loading the side.
The problem is the useEffect
hook is executed after rendering so the function which gets the random image throws an error.
const [allImages, setAllImages] = useState([]);
useEffect(() => {
fetch("https://api.imgflip.com/get_memes")
.then((response) => response.json()
.then((data) => setAllImages(data.data.memes));
}, []);
const [memes, setMemes] = useState([
{ img: getRandomImg() },
]);
function getRandomImg() {
const randomIndex = Math.floor(Math.random() * allImages.length);
console.log("3");
return allImages[randomIndex].url;
}
First thought was to just return the getRandomImg
function if there are no images but nothing is displayed then.
function getRandomImg() {
if (allImages.length === 0) return;
const randomIndex = Math.floor(Math.random() * allImages.length);
console.log("3");
return allImages[randomIndex].url;
}
I found a solution that works by adding another useEffect
with a dependencie on allImages
.
useEffect(() => {
fetch("https://api.imgflip.com/get_memes")
.then((response) => response.json())
.then((data) => setAllImages(data.data.memes));
}, []);
useEffect(() => {
if (allImages.length > 0) {
addImage();
}
}, [allImages]);
const [memes, setMemes] = useState([]);
function getRandomImg() {
const randomIndex = Math.floor(Math.random() * allImages.length);
console.log("3");
return allImages[randomIndex].url;
}
const addImage = () => {
setMemes((prevMemes) => [
...prevMemes,
{
img: getRandomImg(),
},
]);
};
But this seems more like a workaround and this is sometimes adding two pictures instead of one.
Is there any way to get this to work?
CodePudding user response:
Your proposed solution is fine, I would do it this way:
const [allImages, setAllImages] = useState([]);
const [memes, setMemes] = useState([])
useEffect(() => {
fetch("https://api.imgflip.com/get_memes")
.then((response) => response.json()
.then((data) => {
setAllImages(data.data.memes)
const randomIndex = Math.floor(Math.random() * allImages.length);
setMemes([{img: data.data.memes[randomIndex].url}])
// do not use allImages in setMemes, as setState is async
// and the new state of allImages will not be ready yet.
});
}, []);
If you would need the images to be ready before the page is rendered, you can use useLayoutEffect as proposed by Sebastian.
CodePudding user response:
Does memes
need to be in state? If not, useMemo
may be a better fit here:
const [allImages, setAllImages] = useState([]);
useEffect(() => {
fetch("https://api.imgflip.com/get_memes")
.then((response) => response.json()
.then((data) => setAllImages(data.data.memes));
}, [setAllImages]);
const memes = useMemo(() => {
if (allImages.length === 0){
return null;
}
return { img: getRandomImg() },
}, [allImages]);
function getRandomImg() {
const randomIndex = Math.floor(Math.random() * allImages.length);
console.log("3");
return allImages[randomIndex].url;
}
The dependency array [allImages]
passed as the second parameter to useMemo
will cause the in-line function (the first parameter) to be executed one time every time allImages
is updated in state (via the setAllImages
callback). Note that at first, allImages
won't have a value because the fetch()
promise hasn't resolved yet, so you have to guard against that. Usually that means rendering null
or an empty string the first time, and re-rendering once the data has been fetched.