0

I have a page /favorites in my React application and my Axios.post request to setFavArray is giving me an infinte loop when logging it, it is not in useEffect, when it is, the array is only filled on random refreshes. New to react, any idea why the post or useState would cause this?

function Favorites() {
    // console.log(login_State)
    const [loginState, setLoginState] = useState("");
    // const [favoriteID, setFavoriteID] = useState([]);
    // const [favResult, setFavResults] = useState();
    const [favArray, setFavArray] = useState();
    const [resultsData, setResultsData] = useState([]);

    Axios.post("http://localhost:3001/favorites", {
            username: loginState
        })
        .then((response) => {
            setFavArray(response)
            // console.log("FAV ARRAY: ", favArray)
        })

    useEffect(() => {
        Axios.get("http://localhost:3001/login").then((response) => {
            if (response.data.loggedIn === true) {
                setLoginState(response.data.user[0].username)
                // console.log(loginState)
            }
        })

        // fetch('https://api.spoonacular.com/recipes/644366/information?apiKey=blank')
        //   .then(response => response.json())
        //   .then((data) => {
        //       setResultsData(data)
        //       // console.log("REAL: ", resultsData)
        //   });
    }, []);

    console.log("FAV ARRAY: ", favArray)
    console.log("REAL SHIT: ", resultsData)

    return (
        resultsData.map(item => {
            return (
                <div className="favorites-wrapper">
                    <h1> All of your favorites in one place! </h1>
                    <div className="favorite-card">
                    <Col className='mt-5' xs={6} md={4}>
                        <Card className='card-hover'>
                            <Card.Img fluid className='img' variant="top" src={item.image} />
                            <Card.Body className='card-body'>
                                <Card.Title className='title' >{item.title}</Card.Title>
                                <Card.Text>
                                    Some quick example text to build on the card title and make up the
                                    bulk of the card's content.
                                </Card.Text>
                            </Card.Body>
                        </Card>
                    </Col>
                </div>
            </div>
            )
        })
    )
}

export default Favorites;

I tried moving each call in and out of useEffect and no luck. On my back end I'm, just checking a DB to see what 'favorites' the current username has.

  • Making a network request inside your component function but not inside the useEffect hook causes the component to re-render itself every time it receives a response from the network request, which in turn triggers the network request again, leading to an infinite loop. useEffect with empty dependency array triggers once on mount. If you have dependencies in the dependency array, useEffect will still trigger after the component mounts, but as well when any of the dependencies in the array change its value. – piskunovim Mar 20 '23 at 21:33

1 Answers1

1

The Problem

You are getting an infinite loop, because you trigger the post request every time your component gets rendered, which in turn changes the components state, which in turn triggers another render.

Currently, the following is happening step by step:

  1. Your component gets called the first time
  2. The post request is triggered
  3. The useEffect gets triggered, which in turn triggers the get login request
  4. You're logging FAV ARRAY and REAL SHIT (both are still empty, because neither request has finished yet)
  5. You build and return your JSX (effectively nothing) since resultsData is empty.
  6. After some time (the time it takes for the first of the two requests to finish), either setFavArray or setLoginState is called, which triggers the component to rerender, so the steps start over again.

To avoid the infinite loop, you have to make sure to only trigger the requests once. Also, your POST call to favourits is dependent on the login calls result, which also isn't properly taken care of currently. This likely leads to the effect that your data is only fetched properly sometimes (it's a race condition).


A Solution

Let's have a look how to make this work. First, let's take care of the calls. Since we need the result of the first call for the second one, we need to wait for the first one to finish. (This is easier with async/await, see end of post for details). I also recommend moving this part into a separate function. See here:

const fetchFavourits = () => {
    const response = await Axios.get("http://localhost:3001/login");
    if (response.data.loggedIn !== true) {
        return []; // if the user isn't logged in, you probably want to throw an error or handle this somehow. For the demo, we just return an empty array.
    }
    const username = response.data.user[0].username;
    return await Axios.post("http://localhost:3001/favorites", {
        username: username 
    });
}

Note: This function could basically be anything that happens asynchronously.

Back to the main problem, in your component. To trigger the request once, when the component gets rendered the first time, we can use useEffect with an empty dependencies array. See here:

function Favorites() {
    const [loaded, setLoaded] = useState(false); // Keep track whether the data has been loaded. Feel free to remove it if you don't need it.
    const [data, setData] = useState([]);
    
    useEffect(() => {
        // to call an async function in useEffect we need this workaround
        // see end of post for details
        const fetchData = async () => {
            setData( await fetchFavourits() );
            setLoaded(true);
            // The two lines above are basically the same as
            // fetchFavourites().then((data) => { 
            //     setData(data); 
            //     setLoaded(true); 
            // });
        };
        fetchData();
    }, []); // any empty dependencies array will make sure that the useEffect is only called on first render.

    // While the data isn't loaded, you can display some loading indicator
    if( !loaded )
        return <p>Still loading</p>;

    // Now we can be sure that data has finished loading
    console.log(data);

    return <div>
        /* your jsx here */
    </div>
}

How this works

Let's look at this step by step again:

  1. Component gets called the first time
  2. useEffect is called, which in turn calls fetchFavourites(). (JS does not actually wait for it to finish though. The fetchData function essentially gets paused and will be continued sometimes after the rest of the component has been executed)
  3. loaded is false, so we return this loading jsx.
  4. some time passes and fetchFavourites finishes
  5. setData and setLoaded are called. (These change the state of the component, which will make react rerender it)
  6. The component gets called/rendered
  7. We skip useEffect, because it's dependencies haven't changed
  8. loaded is now true, so we continue
  9. You render your component with your actual data

A better solution

Another way to solve the problem (and one I would highly recommend) is using something like TanStack Query. Such libraries will take care of a lot of the pain points of fetching and handling server state.


To read on

For details on async/await, see https://stackoverflow.blog/2019/09/12/practical-ways-to-write-better-javascript/ or How and when to use ‘async’ and ‘await’. It essentially does the same as promise.then, but in a more readable way.

For more details on the async function in useEffect see this: React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing

Fitzi
  • 1,641
  • 11
  • 17