T
TanStack•4y ago
afraid-scarlet

retry on custom useInfiniteQuery with graphQL-request

Hi, i'm trying to achieve a custom hook around useInfiniteQuery that will fetch next page automatically. The endpoint I have can only return me 1000 entities at max for a single request. Depending on some parameters, the request can fail if those 1000 entities are too big, so I may need to reduce the amount fetched if that's the case (depending on the first options in variables). I'm struggling to implement the logic where in case it fail, I can change my graphql-request parameters to lower the number of requested entities. Let see some code:
// useAutoInfiniteQuery.ts
const getEntities = async <T>(
requestExtendedOptions: RequestExtendedOptions
): Promise<T> => {
return request<Record<string, T>>(requestExtendedOptions).then(
(result) => Object.values(result)[0]
);
};

const useAutoInfiniteQuery = <T extends Record<string, unknown>>(
infiniteQueryOptions: UseInfiniteQueryOptions<T[]>,
requestExtendedOptions: RequestExtendedOptions
): UseInfiniteQueryResult<T[]> => {
const query = useInfiniteQuery<T[]>({
retry: (_, error) => {
if (error === 'Size too big') {
// do something to reduce first
}
return true;
},
queryFn: ({ pageParam }) =>
getEntities<T[]>({
...requestExtendedOptions,
variables: {
...requestExtendedOptions.variables,
...pageParam,
},
}),
getNextPageParam: (lastPage, pages) =>
lastPage.length >=
(requestExtendedOptions.variables?.first)
? { id: pages.at(-1)?.at(-1)?.id }
: undefined,
...infiniteQueryOptions,
});

useEffect(() => {
if (!query.isFetchingNextPage && query.hasNextPage) query.fetchNextPage();
}, [query]);

return query;
};

// useGetEntities.ts
const query = useAutoInfiniteQuery<Entities>(
{ queryKey: [`getEntitiesQuery`] },
{
url: 'https://myurl',
document: myQuery,
variables: { ... },
}
);
// useAutoInfiniteQuery.ts
const getEntities = async <T>(
requestExtendedOptions: RequestExtendedOptions
): Promise<T> => {
return request<Record<string, T>>(requestExtendedOptions).then(
(result) => Object.values(result)[0]
);
};

const useAutoInfiniteQuery = <T extends Record<string, unknown>>(
infiniteQueryOptions: UseInfiniteQueryOptions<T[]>,
requestExtendedOptions: RequestExtendedOptions
): UseInfiniteQueryResult<T[]> => {
const query = useInfiniteQuery<T[]>({
retry: (_, error) => {
if (error === 'Size too big') {
// do something to reduce first
}
return true;
},
queryFn: ({ pageParam }) =>
getEntities<T[]>({
...requestExtendedOptions,
variables: {
...requestExtendedOptions.variables,
...pageParam,
},
}),
getNextPageParam: (lastPage, pages) =>
lastPage.length >=
(requestExtendedOptions.variables?.first)
? { id: pages.at(-1)?.at(-1)?.id }
: undefined,
...infiniteQueryOptions,
});

useEffect(() => {
if (!query.isFetchingNextPage && query.hasNextPage) query.fetchNextPage();
}, [query]);

return query;
};

// useGetEntities.ts
const query = useAutoInfiniteQuery<Entities>(
{ queryKey: [`getEntitiesQuery`] },
{
url: 'https://myurl',
document: myQuery,
variables: { ... },
}
);
5 Replies
afraid-scarlet
afraid-scarletOP•4y ago
The problem is that in the retry callback, where I need to lower the first param, how can I manage to somehow persist that first param since it will be also checked in the getNextPageParam to see if there's more entities to fetch ? If I do the following
retry: (_, error) => {
if (error === 'Size too big') {
requestExtendedOptions.variables.first = 300
}
return true;
}
retry: (_, error) => {
if (error === 'Size too big') {
requestExtendedOptions.variables.first = 300
}
return true;
}
I will get the next 300 entities without a problem, but then, when RQ fires getNextPageParam, requestExtendedOptions.variables?.first will be back to 1000, and it will not fetch the next page After further investigation, it seems that I don't understand how does queryFn is used internally.
export const useAutoInfiniteQuery = <T extends Record<string, unknown>>(
infiniteQueryOptions: UseInfiniteQueryOptions<T[], CustomError>,
requestExtendedOptions: RequestExtendedOptions
): UseInfiniteQueryResult<T[], CustomError> => {
const [first, setFirst] = useState<number>(1000);

const query = useInfiniteQuery<T[], CustomError>({
retry: (_, error) => {
console.log(error);
if (CustomError === 'Size too big') {
setFirst(300);
}
return true;
},
queryFn: ({ pageParam, signal }) => {
console.log('queryFn: ', first);
return getEntities<T[]>({
...requestExtendedOptions,
signal: signal,
variables: {
...requestExtendedOptions.variables,
...pageParam,
first: first,
},
});
},
...infiniteQueryOptions,
});

useEffect(() => {
console.log('First has changed: ', first);
}, [first]);

return query;
};
export const useAutoInfiniteQuery = <T extends Record<string, unknown>>(
infiniteQueryOptions: UseInfiniteQueryOptions<T[], CustomError>,
requestExtendedOptions: RequestExtendedOptions
): UseInfiniteQueryResult<T[], CustomError> => {
const [first, setFirst] = useState<number>(1000);

const query = useInfiniteQuery<T[], CustomError>({
retry: (_, error) => {
console.log(error);
if (CustomError === 'Size too big') {
setFirst(300);
}
return true;
},
queryFn: ({ pageParam, signal }) => {
console.log('queryFn: ', first);
return getEntities<T[]>({
...requestExtendedOptions,
signal: signal,
variables: {
...requestExtendedOptions.variables,
...pageParam,
first: first,
},
});
},
...infiniteQueryOptions,
});

useEffect(() => {
console.log('First has changed: ', first);
}, [first]);

return query;
};
This code will give me the following logs: First has changed: 1000 queryFn: 1000 error: [...] First has changed: 300 queryFn: 1000 error: [...] queryFn: 1000 error: [...] etc etc How can I change my query depending on the error in retry's callback ? Can it be achieved only inside retry ?
extended-salmon
extended-salmon•4y ago
you are using first in the queryFn, but it's not part of the query key, so you're getting a stale closure. I don't think this is how you can use react-query. retries are supposed to retry with the same values. If you want something more custom, you can always just do all the things inside the queryFn, like: - await a request - catch error - if it's size to big, make another request with a smaller size, or multiple requests - concatenate all responses to one array the queryFn needs to return one promise that is either fulfilled or rejected, but it is not limited to just one request ...
afraid-scarlet
afraid-scarletOP•4y ago
Thanks for the guiding. Moving the logic I had in the retry directly into the queryFn seems pertinent, since useQuery doesn't need to know about first anymore. But then, what if I want to look for a next page ? getNextPageParam will need to know how many entities queryFn has fetch (1000 or 300). So handling this in the queryFn have some limitations. Is there a way to solve this ? I could do the following check inside getNextPageParam to see if there's more to fetch: lastPage.length >= (requestExtendedOptions.variables?.first ?? 1000) || lastPage.length >= 300 but it start to became too verbose, especially if I need to implement other potential values for first in the futur. I think I misunderstanding some stuff about how core of useQuery works, but can't wrap my head into a good solution.. PS: Just realize you're the author of that amazing blog about react-query, thanks a lot for that, it's awesome
extended-salmon
extended-salmon•4y ago
If you move the fetching to the queryFn - do you really still need an infinite query? Or can it be a normal useQuery?
afraid-scarlet
afraid-scarletOP•4y ago
Depending on some parameters, I could have 50k entities to fetch, and i'm limited by page of 1000, so using normal useQuery is doable by doing the concatenation myself, but if I can stick with useInfiniteQuery, it simplify stuff a lot for me 😅

Did you find this page helpful?