Gonzalo
Gonzalo

Reputation: 882

Custom hook with useEffect (on mount) is executing more than once

I have this hook, that calls a redux action on mount. So my guess is that it should be executed one time. But it is not the case.

useCategories.js

const useCategories = () => {
  /*
    Hook to call redux methods that will fetch the categories 
    of the exercises. Store it in redux store.

    RETURN: loading, error, data
  */
  const mainCategories = useSelector(state => state.categories.specialities);
  const loading = useSelector(state => state.categories.loading);
  const error = useSelector(state => state.categories.error);

  const dispatch = useDispatch();
  const onInitExercises = useCallback(
    () => dispatch(actions.fetchCategories()),
    [dispatch]
  );

  useEffect(() => {
    console.log("[useCategories] --> UseEffect");
    onInitExercises();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [onInitExercises]);

  const data = { specialities: [] };
  if (mainCategories) {
    console.log("Inside If mainCategories");
    for (let key in mainCategories) {
      data["specialities"].push(mainCategories[key]);
     }
  }

  console.log("[useCategories] --> Before Return");
  return [loading, error, data];
};

export default useCategories;

And now I use it inside another component

SideBar

import React, { useState, useEffect, useRef } from "react";
import useCategories from "../hookFetchCategories/useCategories";

const SideBarFilters = props => {

  const [orderForm, setOrderForm] = useState({})
  // Hook to fetch categories from DB
  const [loading, error, categoriesData] = useCategories();

  // Function to update form after categories are fetched
  const updateSpecialitiesData = useRef((formElementKey, data) => {
    console.log("Inside Update");
    // code to update orderForm with data fetched in useCategories()
    setOrderForm(orderFormUpdated);
  });

  useEffect(() => {
    if (!loading && categoriesData && categoriesData["specialities"].length) {
      console.log("Inside IF");
      updateSpecialitiesData("specialities", categoriesData["specialities"]);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [loading, updateSpecialitiesData]);

  // --- Rest of the component
}

When I run the app, in the DEV console I have this console logs

[useCategories] --> Before Return
useCategories.js:23 [useCategories] --> UseEffect
useCategories.js:53 [useCategories] --> Before Return
useCategories.js:53 [useCategories] --> Before Return
useCategories.js:42 Inside if mainCategories
useCategories.js:53 [useCategories] --> Before Return
SideBarFilters.js:61 Inside IF
SideBarFilters.js:36 Inside Update
useCategories.js:42 Inside if mainCategories
useCategories.js:53 [useCategories] --> Before Return

1) I do not understand why the useCategories hook is not executing only once?

That is what I understand of how useEffect in useCategories should behave.

2) If I add in SideBar.js, in the useEffect hook, the dependencie "categoriesData", it starts an infinite loop.

Upvotes: 5

Views: 7526

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187034

I do not understand why the useCategories hook is not executing only once?

All hooks execute everytime the component is rendered, they just do do different things when executed. Your useCategories custom hook, is just a function that happens to call other hooks provided by React.

In this case, everytime the component is rendered, you will get a:

[useCategories] --> Before Return

Because you are calling a function useCategories on render, that logs that message.

However, you will notice that this message:

[useCategories] --> UseEffect

Does appear only once. This is because the useEffect hook (as you have set it up), will only run once, just after first render.

So to answer your first question, it's working exactly as you think it should be working.


If I add in SideBar.js, in the useEffect hook, the dependency categoriesData, it starts an infinite loop.

Dependencies are compared by identity, not value. Think === not ==. This means, in the case of objects, the comparison is asking "is this the same object? If not, run the effect again.".

If you look at your custom hook, you see this line:

const data = { specialities: [] };

This creates a new object every time the hook runs (which is every render). Even though it may have the same values, it will be a different object every render. This means it will invalidate a dependency array every render.

There's lot of ways to fix this, but one way would useMemo which will only create a new object when the values that would change its value change.

const data = useMemo(() => {
   const myFancyData = {} // logic to create your data here
   return myFancyData
}, [dependenciesFor, above, logic, here])

Or come to think of it, why are you rebuilding this data at all? data is just a manually built clone of mainCategories, so why not just:

return [loading, error, mainCategories];

Redux should return the same object from useSelector unless the data has changed.

Upvotes: 3

Related Questions