Home > Back-end >  How can I use data from an API immediately with useEffect?
How can I use data from an API immediately with useEffect?

Time:08-20

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.

  • Related