Custom react hooks infinite recursion, no recusrion break because of Rule of hooks

Viewed 129

I've encountered interesting problem with custom react hooks, rule of hooks, and recursion. I would like to know if I'm doing something wrong. Thanks for you time.

I have a custom hook useServerData(entityIds). This hook internaly calls (depends on) other custom hook useFilters(entityIds). This hook cyclickly depends on useServerData(shorterEntityIds). Maybe it seems like a logical defect, but it is just a recurion because shorterEntityIds are strictly shorter array. Without rule of hooks, I would wrote recursion break like:

let filters = [];
if (shorterEntityIds.length > 0) {
    filters = useFilters(shorterEntityIds)
}

But it is prohibited by rule of hooks (and I understand why). Without the break JavaScript throws Maximum call stack size exceeded.

How would you solve this? How to break the cycle? What is a React idiomatic way?

Thank you.

EDIT: As Bergi requested I'm adding more specific code for this.

// Returns dictionary where key is given filterId and value is QueryResult<FilterRange>
// Where returned FilterRange depends on Filter and prescending Filters for each Filter
export const useFilterRanges = (filterIds) => {
    const filtersById = useFilters(filterIds);
    const prescendingQueryFiltersById = usePrescendingQueryFilters(filterIds);
    const filterRanges = useQueries(filterIds.map((filterId) => {
        const filter = filtersById[filterId];
        const prescendingFilters = prescendingQueryFiltersById[filterId];
        return fetchFilterRange(filter, prescendingFilters);
    }));
    return zipObject(filterIds, filterRanges);
};
// Returns dictionary where key is given filterId and value is Array<QueryFilter>
// Where returned Array<QueryFilter> depends on internal state (order of Filters) and QueryFilters
export const usePrescendingQueryFilters = (filterIds) => {
    const allFilterIds = useAllFilterIds();
    const index = findLastIndex(allFilterIds, (filterId) => includes(filterIds, filterId));

    // allPrecendingIds are always at least one item shorter than filterIds
    const allPrecendingIds = take(allFilterIds, index);
    const queryFiltersById = useQueryFilters(allPrecendingIds);
    return chain(filterIds)
        .keyBy()
        .mapValues((filterId) => {
            const index = indexOf(allFilterIds, filterId);
            const precendingIds = take(allFilterIds, index);
            const queryFilters = precendingIds.map((id) => queryFiltersById[id]);
            return queryFilters.flatMap((queryFilter) => queryFilter ?? []);
        })
        .value();
};
// Returns dictionary where key is given filterId and value is QueryFilter
// Where returned QueryFilter depends on QueryResult<FilterRange> and Filter
export const useQueryFilters = (filterIds) => {
    const filtersById = useFilters(filterIds);
    const rangesById = useFilterRanges(filterIds);
    return chain(filterIds)
        .keyBy()
        .mapValues((filterId) => {
            const filter = filtersById[filterId];
            const range = rangesById[filterId];
            return constructQueryFilters(filter, range);
        })
        .value(); 
};

Note the code is simplified (I've doublechecked but it can contain some typos) and the real project cycle is even bigger, but I believe this is kind of minimal meaningful example. :)

0 Answers
Related