-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Using RTK Infinite Query For Tabular Pagination #4936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
@EskiMojo14 and I have been poking at this. It seems like the issue is actually with an inference problem when combining the way If you try commenting out Per Ben:
So, don't have a specific answer atm, but that seems to be the root of the issue. |
Hey man, Really appreciate you looking into this. You brought up an interesting point about Here's a code snippet of the more refined implementation. We basically realised that we need an API that's more similar to import {
type BaseQueryFn,
type TypedUseInfiniteQuery,
type TypedUseInfiniteQueryStateOptions,
} from '@reduxjs/toolkit/query/react';
import { useEffect, useMemo, useState } from 'react';
import { type CreatePaginatedInfiniteQueryHookConfig } from './create-paginated-infinite-query-hook.types';
export const createPaginatedInfiniteQueryHook = <
ResultType,
QueryArg,
PageParam,
BaseQuery extends BaseQueryFn,
UseInfiniteQuery extends TypedUseInfiniteQuery<
ResultType,
QueryArg,
PageParam,
BaseQuery
> = TypedUseInfiniteQuery<ResultType, QueryArg, PageParam, BaseQuery>,
>(
useInfiniteQuery: TypedUseInfiniteQuery<
ResultType,
QueryArg,
PageParam,
BaseQuery
>,
{
getLimitFromQueryArg,
injectLimitIntoQueryArg,
}: CreatePaginatedInfiniteQueryHookConfig<Parameters<UseInfiniteQuery>[0]>,
) => {
// hook-scoped type aliases
type UseInfiniteQueryParameters = Parameters<UseInfiniteQuery>;
type UsePaginatedQueryArg = UseInfiniteQueryParameters[0];
// construct the type instead of relying on `Parameters`
type UsePaginatedQuerySubscriptionOptions = TypedUseInfiniteQueryStateOptions<
ResultType,
QueryArg,
PageParam,
BaseQuery
>;
return (
initialLimit: number,
initialQueryArg: UsePaginatedQueryArg,
subscriptionOptions?: UsePaginatedQuerySubscriptionOptions,
) => {
const [pageIndex, setPageIndex] = useState(0);
const [queryArg, setQueryArg] = useState(() =>
injectLimitIntoQueryArg(initialQueryArg, initialLimit),
);
const result = useInfiniteQuery(queryArg, subscriptionOptions);
const { fetchNextPage, refetch, data, isFetchingNextPage } = result;
const { pages } = data ?? { pages: [] };
const currentPage = useMemo(() => {
if (pages) {
return pages[pageIndex];
}
}, [pageIndex, pages]);
useEffect(() => {
refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryArg]);
/** Go forwards by 1 page. */
const getNextPage = async () => {
if (isFetchingNextPage) return;
const nextPageAlreadyFetched = pageIndex + 1 <= pages.length - 1;
if (nextPageAlreadyFetched) {
setPageIndex((prevPageIndex) => prevPageIndex + 1);
} else if (result.hasNextPage) {
await fetchNextPage();
setPageIndex((prevPageIndex) => prevPageIndex + 1);
}
};
/** Go back by 1 page. */
const getPrevPage = async () => {
if (pageIndex <= 0) return;
setPageIndex((prevPageIndex) => prevPageIndex - 1);
};
/**
* Changing the limit will always reset pagination to `pageIndex` 0.
*/
const changeLimit = async (newLimit: number) => {
setPageIndex(0);
setQueryArg((prevQueryArg) =>
injectLimitIntoQueryArg(prevQueryArg, newLimit),
);
};
/**
* Call when `QueryArg` for `useInfiniteQuery` changes.
*/
const refetchWithNewArgs = async (newQueryArg: UsePaginatedQueryArg) => {
// use the new limit from `newQueryArg` if it exists - otherwise preserve the old limit
const newLimit =
getLimitFromQueryArg(newQueryArg) ??
getLimitFromQueryArg(queryArg) ??
initialLimit;
setPageIndex(0);
setQueryArg(injectLimitIntoQueryArg(newQueryArg, newLimit));
};
return {
getNextPage,
getPrevPage,
changeLimit,
refetchWithNewArgs,
currentPage,
paginationState: {
pageIndex,
pageSize: getLimitFromQueryArg(queryArg) ?? initialLimit,
canGetNextPage:
(pageIndex + 1 <= pages.length - 1 || result.hasNextPage) &&
!isFetchingNextPage,
canGetPrevPage: pageIndex > 0,
},
infiniteQueryResults: result,
};
};
}; An interesting observation is that we noticed the reference to import {
type BaseQueryFn,
type TypedUseInfiniteQuery,
type TypedUseInfiniteQueryStateResult,
} from '@reduxjs/toolkit/query/react';
import { useMemo, useState } from 'react';
import { type CreatePaginatedInfiniteQueryHookConfig } from './create-paginated-infinite-query-hook.types';
type PaginationContext<QueryArg> = {
pageIndex: number;
queryArg: QueryArg;
};
export const createPaginatedInfiniteQueryHook = <
ResultType,
QueryArg,
PageParam,
BaseQuery extends BaseQueryFn,
UseInfiniteQuery extends TypedUseInfiniteQuery<
ResultType,
QueryArg,
PageParam,
BaseQuery
> = TypedUseInfiniteQuery<ResultType, QueryArg, PageParam, BaseQuery>,
>(
useInfiniteQuery: TypedUseInfiniteQuery<
ResultType,
QueryArg,
PageParam,
BaseQuery
>,
{
getLimitFromQueryArg,
injectLimitIntoQueryArg,
}: CreatePaginatedInfiniteQueryHookConfig<Parameters<UseInfiniteQuery>[0]>,
) => {
// hook-scoped type aliases
type UseInfiniteQueryParameters = Parameters<UseInfiniteQuery>;
type UsePaginatedQueryArg = UseInfiniteQueryParameters[0];
type UsePaginatedQuerySubscriptionOptions = UseInfiniteQueryParameters[1];
type UsePaginatedQueryResult = TypedUseInfiniteQueryStateResult<
ResultType,
QueryArg,
PageParam,
BaseQuery
> &
ReturnType<UseInfiniteQuery>;
return (
initialLimit: number,
initialQueryArg: UsePaginatedQueryArg,
subscriptionOptions?: UsePaginatedQuerySubscriptionOptions,
) => {
const [{ pageIndex, queryArg }, setPaginationContext] = useState<
PaginationContext<UsePaginatedQueryArg>
>({
pageIndex: 0,
queryArg: injectLimitIntoQueryArg(initialQueryArg, initialLimit),
});
const result = useInfiniteQuery(
queryArg,
subscriptionOptions,
) as UsePaginatedQueryResult;
const { fetchNextPage, refetch } = result;
const data = 'data' in result ? result.data : undefined;
const { pages } = data ?? { pages: [] };
/** Go forwards by 1 page. */
const getNextPage = async () => {
if (pageIndex + 1 > pages.length - 1 && result.hasNextPage) {
await fetchNextPage();
} else if (pageIndex + 1 > pages.length - 1) {
return;
}
setPaginationContext(({ pageIndex, ...rest }) => ({
...rest,
pageIndex: pageIndex + 1,
}));
};
/** Go back by 1 page. */
const getPrevPage = async () => {
if (pageIndex <= 0) return;
setPaginationContext(({ pageIndex, ...rest }) => ({
...rest,
pageIndex: pageIndex - 1,
}));
};
/**
* Changing the limit will always reset pagination to `pageIndex` 0.
*/
const changeLimit = async (newLimit: number) => {
setPaginationContext({
pageIndex: 0,
queryArg: injectLimitIntoQueryArg(queryArg, newLimit),
});
/**
* `refetch` cannot occur within the same execution context as `setPaginationContext`.
* When `refetch` is called, `useInfiniteQuery` needs to have already been rendered with
* the new `queryArg`. Calling it synchronously with `setPaginationContext` in the
* event handler will batch the updates - such that `refetch` is called before
* `useInfiniteQuery` has been rendered with the new `queryArg`.
*
* Calling `refetch` in `setTimeout` will schedule `refetch` to be called after the
* update to `paginationContext` has been rendered. This ensures that the query is called
* with the latest `queryArg`.
*/
return new Promise((resolve) => {
setTimeout(async () => {
const result = await refetch();
resolve(result);
});
});
};
/**
* Call when `QueryArg` for `useInfiniteQuery` changes.
*/
const refetchWithNewArgs = async (newQueryArg: UsePaginatedQueryArg) => {
// use the new limit from `newQueryArg` if it exists - otherwise preserve the old limit
const newLimit =
getLimitFromQueryArg(newQueryArg) ??
getLimitFromQueryArg(queryArg) ??
initialLimit;
setPaginationContext({
pageIndex: 0,
queryArg: injectLimitIntoQueryArg(newQueryArg, newLimit),
});
/**
* `refetch` cannot occur within the same execution context as `setPaginationContext`.
* When `refetch` is called, `useInfiniteQuery` needs to have already been rendered with
* the new `queryArg`. Calling it synchronously with `setPaginationContext` in the
* event handler will batch the updates - such that `refetch` is called before
* `useInfiniteQuery` has been rendered with the `newQueryArg`.
*
* Calling `refetch` in `setTimeout` will schedule `refetch` to be called after the
* update to `paginationContext` has been rendered. This ensures that the query is called
* with the latest `queryArg`.
*/
return new Promise((resolve) => {
setTimeout(async () => {
const result = await refetch();
resolve(result);
});
});
};
const currentPage = useMemo(() => {
if (pages) {
return pages[pageIndex];
}
}, [pageIndex, pages]);
return {
getNextPage,
getPrevPage,
changeLimit,
refetchWithNewArgs,
currentPage,
paginationState: {
pageIndex,
pageSize: getLimitFromQueryArg(queryArg) ?? initialLimit,
canGetNextPage: pageIndex + 1 <= pages.length - 1 || result.hasNextPage,
canGetPrevPage: pageIndex > 0,
},
infiniteQueryResults: result,
};
};
}; We deferred calling Probably something worth looking into? |
Yeah, just merged #4937 , which should make both the fetch methods and Per the behavior, the |
Uh oh!
There was an error while loading. Please reload this page.
Hi Redux maintainers,
Very grateful for the new RTK Infinite Query API. I think this solves a lot of the pain points with manually merging paginated requests and cursor management. However, given that it's mainly built for infinite scrolling UIs, we've had to implement our own logic to make this API usable for tabular pagination.
The code snippet below highlights how we're trying to go about this. Almost every table in our app are currently server-side paginated and so we're trying to opt for a higher-order hook (hook factory) pattern to wrap the infinite query hook with all the extra components that our tables need.
However, there are some pain points when working with the types in the current version of the package. Namely,
TypedUseInfiniteQuery
under generic conditions only returns:refetch
fetchNextPage
fetchPreviousPage
I don't understand why
data
for instance, could not just be returned asInfiniteData<ResultType, PageParam>
or why the booleans likeisFetching
andhasNextPage
are not there.Any advice on how to go about this? Perhaps this is the wrong pattern to use?
The text was updated successfully, but these errors were encountered: