Search code examples
reactjsreact-hooksuse-effectuse-context

useEffect stuck in infinite loop with useContext


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?


Solution

  • 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