Home > Blockchain >  useEffect stuck in infinite loop with useContext
useEffect stuck in infinite loop with useContext

Time:10-28

I have a functional component Pets that shows all of the pets of the current logged in user. Inside of my Pets component I am using useEffect and useState to achieve this.

const [pets, setPets] = useState([]);

useEffect(() => {
    const fetchPets = async () => {
      try {
        const { data } = await axios.get('/pet/mypets');
        setPets(data.pets);
      } catch (error) {
        console.log(error);
      }
    };

    fetchPets();
  }, []);

I then render all the pets in a table row by row. Problem is that when I tried doing this using useContext it ended up in an infinite loop. For instance, I have a file called petsContext with the following code...

import React from 'react';

const PetsContext = React.createContext({
  pets: [],
  onRegister: (first_name, last_name, breed, age, weight) => {},
  onUpdate: (first_name, last_name, breed, age, weight) => {},
  onDelete: () => {},
  onFetch: () => {},
});

export default PetsContext;

I have a petsProvider file with the following...

import React, { useReducer } from 'react';
import PetContext from './pets-context';
import axios from 'axios';

const initialState = {
  pets: [],
  message: '',
  messageType: '',
};

const petReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_PETS_SUCCESS':
      return {
        ...state,
        pets: action.payload,
      };
    case 'REGISTER_PET_SUCCESS':
      if (state.pets === undefined) {
        state.pets = [];
      }

      return {
        ...state,
        pets: [action.payload.pet, ...state.pets],
        message: action.payload.message,
      };
    case 'REGISTER_PET_FAIL':
      return {
        ...state,
        message: 'Unable to register pet!',
      };
    default:
      return initialState;
  }
};

const PetProvider = (props) => {
  const [petState, dispatchPetAction] = useReducer(petReducer, {
    petReducer,
    initialState,
  });

  const registerHandler = async (first_name, last_name, breed, age, weight) => {
    const config = {
      headers: {
        'Content-Type': 'application/json',
      },
    };

    age = parseInt(age);
    weight = parseFloat(weight);

    const body = JSON.stringify({ first_name, last_name, breed, age, weight });

    try {
      const { data } = await axios.post('/pet/register', body, config);

      dispatchPetAction({ type: 'REGISTER_PET_SUCCESS', payload: data });
    } catch (error) {
      console.log(error.response);
      // dispatchPetAction({
      //   type: 'REGISTER_PET_FAIL',
      //   payload: {
      //     message: error.response.data.message,
      //     messageType: 'danger',
      //   },
      // });
    }
  };

  const fetchHandler = async () => {
    try {
      const { data } = await axios.get('/pet/mypets');
      dispatchPetAction({ type: 'FETCH_PETS_SUCCESS', payload: data.pets });
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <PetContext.Provider
      value={{
        pets: petState.pets,
        onRegister: registerHandler,
        onFetch: fetchHandler,
      }}
    >
      {props.children}
    </PetContext.Provider>
  );
};

export default PetProvider;

and in my Pets component instead of having what I showed before I had the following...

const petsContext = useContext(PetsContext);

useEffect(() => {
  petsContext.onFetch();
}, [petsContext]);

// now I want to just access my pets by `petsContext.pets`
// which works but ends up in an infinite loop and I am not sure why.

How can I fix this infinite loop and why is it happening?

CodePudding user response:

The problem of the infinite renders with useEffect is because of the dependency array. If you are updating state inside the effect, but the effect is dependent on the state you're updating, then the effect is called anytime anything in your petsContext changes.

That means that the effect is called when context changes, then it changes the context, which calls itself again ad infinitum until you get a Stack Overflow (rimshot).

To get around this, decide what you want this effect to be dependent on, and minimize it to just that object. onFetch is declared as an empty function in your example, so I'm not 100% sure what it's doing or I would advise you further.

CodePudding user response:

The infinite loop start from your context.

  1. Your context value creates for every render.
  2. Because your context value is changed, the effect calls fetch
  3. Fetch update the state, that continue trigger the render. Because the render is triggered, the First will be fire.

To fix it:

  // wrap fetchHandler in useCallback
  const fetchHandler = useCallback(async () => {
    try {
      const { data } = await axios.get('/pet/mypets');
      dispatchPetAction({ type: 'FETCH_PETS_SUCCESS', payload: data.pets });
    } catch (err) {
      console.log(err);
    }
  }, [dispatchPetAction]);

  const { onFetch } = useContext(PetsContext);

  // this effect should depend on onFetch, not petsContext
  // now, it will only being call if dispatchPetAction is changed
  // dispatchPetAction -> fetchHandler -> useEffect
  useEffect(() => {
    onFetch();
  }, [onFetch]);

https://reactjs.org/docs/hooks-reference.html#usecallback

  • Related