Reputation: 882
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
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