Search code examples
reactjsreact-hooksrender

custom hook create infinite loop


This hook create an inifite loop. I don't understnd why, since my dependencies array is set.

Error : Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

Custom hook :

export const useListGuessers = () => {
    const [list, setList] = useState([]);

    const hasMarketing = UserHelper.hasAuthorization(AUTHORIZATION_MARKETING);
    const hasTechnical = UserHelper.hasAuthorization(AUTHORIZATION_TECHNICAL);

    const dashboardGroups = new DashboardGroups({hasMarketing, hasTechnical});

    const guessers = [
        ...dashboardGroups.appProductGroup(),
        ...dashboardGroups.articlesGroup(),
        ...dashboardGroups.mediasGroup(),
        ...dashboardGroups.productsGroup(),
        ...dashboardGroups.orderableProductsGroup(),
        ...dashboardGroups.typesGroup(),
        ...dashboardGroups.usersGroup(),
        ...dashboardGroups.othersGroup(),
        ...dashboardGroups.userManagementGroup(),
    ];

    const filteredGuesser = guessers
        .filter(({canShow}) => canShow)
        .map((guesser: any) => {
            return {
                label: guesser.label ?? guesser.value.options.label,
                link: (guesser.operation && `user-management/${guesser.operation}`) ?? guesser.value.name,
            };
        })
        .sort((a, b) => a.label.localeCompare(b.label));

    useEffect(() => {
        filteredGuesser && setList(filteredGuesser);
    }, [filteredGuesser]);

    return list;
};

The class :

export class DashboardGroups {
    authorizations: {hasMarketing: boolean; hasTechnical: boolean};

    constructor(authorizations: {hasMarketing: boolean; hasTechnical: boolean}) {
        this.authorizations = authorizations;
    }

    // [all groups comes here...]

    getGroups = () => {
        // return an object for each groups with labels, and the group as "children"
    };
}

Solution

  • Since filteredGuesser calculates on each re-render, which triggers useEffect(..., [filterGuesser] which causes re-render... so it loops.

    The easiest straighforward solution is to ensure reference equality for filteredGuesser with useMemo. Then it will be referentially the same until guessers is changed:

    const filteredGuesser = useMemo(() => 
      guessers
        .filter(({canShow}) => canShow)
        .map((guesser: any) => {
          label: guesser.label ?? guesser.value.options.label,
          link: (guesser.operation && `user-management/${guesser.operation}`) ?? guesser.value.name,
         })
         .sort((a, b) => a.label.localeCompare(b.label))
    , [guessers]);
    

    However, I think the better solution would be reconsider need in

    useEffect(() => 
    ...
     setList(filteredGuesser)
    

    This storing ready for use calculation into state does not seem reasonable to me. I think you better use filteredGuesser directly, instead of storing it into list state

    Beta docs for useMemo

    Referential equality aka strict equality on MDN