Skip to content

Instantly share code, notes, and snippets.

@aparx
Last active January 25, 2023 01:26
Show Gist options
  • Save aparx/195f30f442ece872feecb019da11fbc1 to your computer and use it in GitHub Desktop.
Save aparx/195f30f442ece872feecb019da11fbc1 to your computer and use it in GitHub Desktop.
Enables individual page data remapping and removal for a set of queries using react-query (only through useInfiniteQuery)
import {Query, QueryCache, QueryClient, QueryFilters} from "@tanstack/react-query";
import {InfiniteData} from "@tanstack/query-core/build/lib/types";
export type InfiniteQueryData<U, V> = InfiniteData<U | V | undefined>;
export type InfinitePageMapperParam<OldPageT, NewPageT> = {
page: OldPageT | NewPageT,
index: number,
param?: unknown,
query?: Query<unknown, unknown, InfiniteQueryData<OldPageT, NewPageT>>,
};
export type InfinitePageMapper<OldPageT, NewPageT> = (
data: InfinitePageMapperParam<OldPageT, NewPageT>
) => OldPageT | NewPageT | undefined;
export type InfiniteQueryArray<OldPageT, NewPageT>
= Array<Query<unknown, unknown, InfiniteQueryData<OldPageT, NewPageT>>>;
export function ensureQueryHasInfiniteData(
query: Query<unknown, unknown, any>
): query is Query<unknown, unknown, InfiniteData<any>> {
const {data}: any = query.state;
return "pages" in data && "pageParams" in data;
}
export const updateInfinitePagesInClient = <OldPageT = any, NewPageT = OldPageT>(
queryClient: QueryClient,
mapper: InfinitePageMapper<OldPageT, NewPageT>,
filters?: QueryFilters
) => updateInfinitePagesInCache(queryClient.getQueryCache(), mapper, filters);
export const updateInfinitePagesInCache = <OldPageT = any, NewPageT = OldPageT>(
queryCache: QueryCache,
mapper: InfinitePageMapper<OldPageT, NewPageT>,
filters?: QueryFilters
) => updateInfinitePages(
queryCache.findAll({
...filters,
predicate: (query) => {
if (!ensureQueryHasInfiniteData(query))
return false;
if (filters && filters.predicate)
return filters.predicate(query);
return true;
},
}) as InfiniteQueryArray<OldPageT, NewPageT>,
mapper
);
export const updateInfinitePages = <OldPageT = any, NewPageT = OldPageT>(
queries: InfiniteQueryArray<OldPageT, NewPageT>,
mapper: InfinitePageMapper<OldPageT, NewPageT>
) => queries.forEach(query => {
const data = query.state.data;
if (!data) return; // data is invalid, skip
const newPages: (OldPageT | NewPageT | undefined)[] = [];
const newParams: unknown[] = [];
data.pages.forEach((page, index) => {
if (page == null) return;
const param = data.pageParams?.[index];
const newPage = mapper({
page, index, param, query
});
if (!newPage) return;
newPages.push(newPage);
newParams.push(param);
});
query.setData({
pages: newPages,
pageParams: newParams
} satisfies InfiniteQueryData<OldPageT, NewPageT>);
});
@aparx
Copy link
Author

aparx commented Jan 25, 2023

The problem

Sometimes you may want to undo (or reverse) the fetching of pages with the useInfiniteQuery hook without resetting all fetched queries, since that would require a refetch everytime an undone page should be displayed again.

I have yet to find a better solution than what is done in this little gist. Read more about why default methods may not work below in the Examples.

Usages and definitions

Function updateInfinitePages
Used to map the pages in the specified Query array.

Function updateInfinitePagesInCache
Used to map all pages whose queries' data (provided by given cache) are an instance of InfiniteData.
This is primarily done with an additional filter, besides the optional filter passed to the function, that only exposes queries to the mapper whose data object have the required properties of type InfiniteData (which is used as the query data type by default in react-query)

Function updateInfinitePagesInClient
Invokes updateInfinitePagesInCache and passes the client's query cache as the cache to provide the queries.

Examples

Assume we create an useInfiniteQuery hook with a button ("Show more") that when clicked loads more elements using any arbitrary Rest-API that then queries our backend database. There's an additional button ("Show less") that is shown whenever there is more than one page fetched. The "Show less" button will remove all pages except the first one.

For this example we allocate a new QueryClient called queryClient that we only use for this hook. This is not necessary since we can easily use query keys later on.

Default approach

The approach that comes to mind at first would be to simply reset and/or invalidate the cache, which would force a refetch from react-query for our first page. But this is unnecessary, since we already have our data handy. Thus, the other approach is what we seek.

The other approach

Same assumption as above, but we use following method on our "Show less" button, when we click it.

function removeAllPagesExceptFirst() {
    updateInfinitePagesInClient<SomeOrdinaryData>(queryClient,
        ({page, index}) => index == 0 ? page : undefined)
}

If you want to use a global queryClient and not just a local one that is just memoized for this hook, you can append a filters argument to the updateInfinitePagesInClient invocation, where you can specify the target key to reevaluate.

Now, whenever we click our "Show less" button it simply just remaps the page content for each page to the value returned by our mapper (the closure we pass). This mapper implementation states that whenever our iteration index is zero (which always represents the first page) we actually do not change the page content, but if the index is not zero we remove our page (including the param) entirely from the query's data.

Good to know: the generic type argument SomeOrdinaryData we pass above is a custom type that defines the shape of our current pages and our new pages. If you want to differentiate the shape of the old and new pages you can simply add another generic argument that then represents the new pages' shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment