Sun23
Sun23

Reputation: 87

How to execute a function AFTER an API-Call was made with setState within useEffect?

i am a React newbie (30h learning) and some basic Javascript background. Now i am learning with a course, and tried to "leave" the path. But i am curious how my intended goal could be achieved.

There is a Memegenerator who get all the images of memes from an API in the beginning of the rendering. This is solved with an useEffect-Hook. Now i want that the function getMemeImage() is running ONCE at the beginning AFTER the API-Call was made and the state was updated (this is not part of the course, but i want to try it anyway).

But its giving me an error: "Can't perform a React state update on an unmounted component"

While my research i found that things like didMount(tbh i not understand) and so on are not the "modern" way of react with primarily using hooks and you can simply use an second useEffect. But that is unfortunately not working for me.

How can i solve this and is there an "simple" way for a beginner or it is advanced stuff? I thought maybe to use a timeoutfunction, but seems to be very bad coding.

import React from "react"

export default function Meme() {
    const [meme, setMeme] = React.useState({
        topText: "",
        bottomText: "",
        randomImage: "" 
    })
    const [allMemes, setAllMemes] = React.useState([])

React.useEffect(() => {   /* first useEffect */
    fetch("https://api.imgflip.com/get_memes")
        .then(res => res.json())
        .then(data => setAllMemes(data.data.memes))
}, [])




React.useEffect(() => {   /* This should run after the setAllMemes in the first useEffect was complete */

    getMemeImage()
}, [allMemes])



function getMemeImage() {
    const randomNumber = Math.floor(Math.random() * allMemes.length)
    const url = allMemes[randomNumber].url
    setMeme(prevMeme => ({
        ...prevMeme,
        randomImage: url
    }))
    
}

function handleChange(event) {
    const {name, value} = event.target
    setMeme(prevMeme => ({
        ...prevMeme,
        [name]: value
    }))
}

return (
    <main>
        <div className="form">
            <input 
                type="text"
                placeholder="Top text"
                className="form--input"
                name="topText"
                value={meme.topText}
                onChange={handleChange}
            />
            <input 
                type="text"
                placeholder="Bottom text"
                className="form--input"
                name="bottomText"
                value={meme.bottomText}
                onChange={handleChange}
            />
            <button 
                className="form--button"
                onClick={getMemeImage}
            >
                Get a new meme image 🖼
            </button>
        </div>
        <div className="meme">
            <img src={meme.randomImage} className="meme--image" />
            <h2 className="meme--text top">{meme.topText}</h2>
            <h2 className="meme--text bottom">{meme.bottomText}</h2>
        </div>
    </main>
)
}

Upvotes: 2

Views: 3810

Answers (2)

Evren
Evren

Reputation: 4410

You can just add control to your second useEffect, it will solve your issue. This is working sample https://codesandbox.io/s/heuristic-matan-3kdbmv

React.useEffect(() => {
    if (allMemes.length > 0) {
      getMemeImage();
    }
  }, [allMemes]);

Upvotes: 1

Alon Barenboim
Alon Barenboim

Reputation: 438

First of all, "Can't perform a React state update on an unmounted component" is a warning, not an error.

In order to call 'getMemeImage' only after the first useEffect hook execution, you can check the value of a binary flag that changes after the first useEffect was executed and the response of the async function was recieved.

import React from "react";

export default function Meme() {
  const [meme, setMeme] = React.useState({
    topText: "",
    bottomText: "",
    randomImage: ""
  });
  const [allMemes, setAllMemes] = React.useState([]);
  const isInitialRender = React.useRef(true);

  React.useEffect(() => {
    fetch("https://api.imgflip.com/get_memes")
      .then((res) => res.json())
      .then((data) => {

        isInitialRender.current = false;
        setAllMemes(data.data.memes);
      });
  }, []);

  React.useEffect(() => {
 /* Check if it is called before the first useEffect data fetching was completed */
    if (!isInitialRender.current) getMemeImage();
  }, [allMemes]);

  function getMemeImage() {
    const randomNumber = Math.floor(Math.random() * allMemes.length);
    const url = allMemes[randomNumber].url;
    setMeme((prevMeme) => ({
      ...prevMeme,
      randomImage: url
    }));
  }
}

Using the useRef hook persists the value it references between renders.

Upvotes: 2

Related Questions