Krzysztof Mansz
Krzysztof Mansz

Reputation: 113

Incorrect dependency behavior in hook useEffect?

I've got a problem with infinite loop inside useEffect. I want to make new request only when id dependency changes. When I not pass data I've got a problem with setData. My data state is not updated. When I pass data to the dependency I've got an infinite loop. How to fix it and why?

import React, {useEffect, useState} from 'react';
import LeftArrow from './LeftArrow';
import RightArrow from './RightArrow';
import SlideItem from './SlideItem';

const Slide = () => {
    const [id, setId] = useState(0);
    const [data, setData] = useState({currencies:[], isFetching:false});

    const images = [
        "https://images.pexels.com/photos/672532/pexels-photo-672532.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
        "https://images.pexels.com/photos/773471/pexels-photo-773471.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
        "https://images.pexels.com/photos/64271/queen-of-liberty-statue-of-liberty-new-york-liberty-statue-64271.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
    ];

    useEffect(()=> {
        const getCurrentCurrency = async () => {
            try{
                setData({currencies: data.currencies, isFetching: true});
                console.log("data", data.isFetching);
                const currencyArr = [];
                const response = await fetch(`https://api.exchangeratesapi.io/latest?base=GBP`);
                const responseData  = await response.json();
                console.log(responseData);
                const {EUR:euro ,CHF:franc, USD: dolar} = responseData.rates;
                currencyArr.push(euro,franc,dolar);
                console.log(currencyArr);
                setData({currencies: currencyArr, isFetching: false});
                console.log("currencies", data);     
            }
            catch (e) {
                console.log(e);
                setData({currencies: data.currencies, isFetching: false});
            }
        };
        getCurrentCurrency();
    }, [id]);



    const goToPrevSlide = () => {
      //  id === 0 ? setId(2) : setId(id-1);
    }
    const goToNextSlide = () =>{
      //  id === 2 ? setId(0) : setId(id+1);
    }


    return(

        <div className="slide">
            <div className="slide-wrapper"
                style={{
                    transform: `translateX(500px)`,
                    transition: 'transform ease-out 0.45s'
                  }}
            >
             {
                 currencies.map((currency, i, images) => (
                  <SlideItem currency={currency} key={i} imageUrl={images[i]} />
             ))
            }
            </div>
            <LeftArrow 
                 goToPrevSlide={goToPrevSlide}
            />
            <RightArrow 
                 goToNextSlide={goToNextSlide}    
            />  
        </div>
    );
}

export default Slide;

Upvotes: 0

Views: 373

Answers (2)

Willman.Codes
Willman.Codes

Reputation: 1413

Here, even though you are setting state with its own value, when the useEffect finishes it thinks you have updated state with a new value as data.currencies will be set during the useEffect, this causes the loop when adding it as a dependancy.

If you want to keep the code as you have you can add // eslint-disable-next-line to the line before.

Reason: In this case as you know that you do not want to run this useEffect when data.currencies is changed (as that is the point of this useEffect)

Another option, since you are in fact trying to change currencies to a loading state, You can simply just change:

setData({currencies: data.currencies, isFetching: true});
-and-
setData({currencies: data.currencies, isFetching: false});

to be:

setData({isFetching: true});
-and-
setData({isFetching: false});

(Keep in mind that setState is not immediate as set state goes to the javascript queue so its best not to rely on this)

Full (slightly modified return statement) working code:

import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom'

const Slide = () => {
    const [id, setId] = useState(0);
    const [data, setData] = useState({currencies:[], isFetching:false});

    const images = [
       "https://images.pexels.com/photos/672532/pexels-photo-672532.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
       "https://images.pexels.com/photos/773471/pexels-photo-773471.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
       "https://images.pexels.com/photos/64271/queen-of-liberty-statue-of-liberty-new-york-liberty-statue-64271.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
    ];

    useEffect(()=> {
        const getCurrentCurrency = async () => {
            try{
                setData({isFetching: true});
                const currencyArr = [];
                const response = await fetch(`https://api.exchangeratesapi.io/latest?base=GBP`);
                const responseData  = await response.json();
                const {EUR:euro ,CHF:franc, USD: dolar} = responseData.rates;
                currencyArr.push(euro,franc,dolar);
                setData({currencies: currencyArr, isFetching: false});
            }
            catch (e) {
                setData({isFetching: false});
            }
        };
        getCurrentCurrency();
    }, [id]);



    const goToPrevSlide = () => {
         id === 0 ? setId(2) : setId(id-1);
    }
    const goToNextSlide = () =>{
         id === 2 ? setId(0) : setId(id+1);
    }


    return(
        <div>
            <div className="slide">
                <div>
                    {id}
                </div>
                {JSON.stringify(data)}
                <div>
                    <button onClick={goToPrevSlide}>Prev</button>
                    <button onClick={goToNextSlide}>Next</button>  
                </div>
            </div>
        </div>
    );
}

Upvotes: 1

HMR
HMR

Reputation: 39320

Here is an example how you could do it with useDispatch, it's more code but you can take the reducer out of the component in a separate file and should put action types and action creators in a separate file as well.

const Slide = () => {
  const [id, setId] = React.useState(0);
  const [data, dispatch] = React.useReducer(
    //you can move this function to a separate file and import it
    (state, action) => {
      const { type } = action;
      if (type === 'LOAD') {//action types should be defined as constants
        return {
          ...state,
          loading: true,
        };
      }
      if (type === 'RECEIVED') {
        const currencies = action.payload;
        return {
          currencies,
          loading: false,
        };
      }
      if (type === 'ERROR') {
        return {
          ...state,
          loading: false,
        };
      }
      return state;
    }
  );

  const images = [
    'https://images.pexels.com/photos/672532/pexels-photo-672532.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
    'https://images.pexels.com/photos/773471/pexels-photo-773471.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
    'https://images.pexels.com/photos/64271/queen-of-liberty-statue-of-liberty-new-york-liberty-statue-64271.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
  ];

  useEffect(() => {
    const getCurrentCurrency = async () => {
      try {
        //should create action creator functions so you can do
        //dispatch(load()) put the action creators in separate file
        //usually called actions.js
        dispatch({ type: 'LOAD' });
        console.log('loading');
        //not sure how this depends on id since you are not using id
        //  in the request or anywhere in the effect
        const response = await fetch(
          `https://api.exchangeratesapi.io/latest?base=GBP`
        );
        const responseData = await response.json();
        console.log(responseData);
        const {
          EUR: euro,
          CHF: franc,
          USD: dollar,
        } = responseData.rates;
        const currencies = [euro, franc, dollar];
        console.log(currencies);
        dispatch({
          type: 'RECEIVED',
          payload: currencies,
        });
        console.log('currencies', currencies);
      } catch (e) {
        console.log(e);
        dispatch({
          type: 'ERROR',
        });
      }
    };
    getCurrentCurrency();
  }, [id]);

  //other stuff and return jsx

};

Before you dispatch you should probably check if the component is still mounted, if the component will not unmount then this is not a problem.

Upvotes: 0

Related Questions