client/helpers/usePaginatedQuery.ts
import { NetworkStatus } from "@apollo/client";
import React from "react";
import { useLocation } from "react-router";
import { useNavigateToUrl } from "../helpers";
type HookFunction = (args: {
fetchPolicy?: any;
variables: {
input: {
before?: string;
after?: string;
first?: number;
last?: number;
filter?: Record<string, any>;
};
};
}) => {
networkStatus: NetworkStatus;
loading: boolean;
data?: Record<string, any>;
};
type NotNullOrUndefined<T> = Exclude<T, null | undefined>;
type HookFilters<T extends HookFunction> = NotNullOrUndefined<
NotNullOrUndefined<Parameters<T>[0]["variables"]>["input"]
>["filter"];
type GetConnectionFn<T extends HookFunction, TNode = any> = (
data: ReturnType<T>["data"]
) =>
| undefined
| {
pageInfo: {
totalCount: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
edges: {
cursor: string;
node: TNode;
}[];
};
export function usePaginatedQuery<
THookFn extends HookFunction,
TGetConnection extends GetConnectionFn<THookFn>
>(
useQuery: THookFn,
{
filter,
perPage = 3,
getConnection,
}: {
filter?: HookFilters<THookFn>;
perPage?: number;
getConnection: TGetConnection;
}
): {
loading: boolean;
hasNextPage: boolean;
hasPreviousPage: boolean;
totalCount: number;
nextPageLink: string;
previousPageLink: string;
nextPage: () => void;
previousPage: () => void;
nodes: NotNullOrUndefined<
NotNullOrUndefined<ReturnType<TGetConnection>>["edges"]
>[number]["node"][];
} {
const navigate = useNavigateToUrl();
const { search, pathname: baseUrl } = useLocation();
const queryParams = React.useMemo(
() => new URLSearchParams(search),
[search, baseUrl]
);
const cursor = queryParams.get("cursor");
const beforeOrAfter = queryParams.get("beforeOrAfter") || "after";
const firstOrLast = queryParams.get("firstOrLast") || "first";
const { data, networkStatus } = useQuery({
fetchPolicy: "cache-and-network",
variables: {
input: {
[beforeOrAfter]: cursor,
[firstOrLast]: perPage,
filter,
},
},
});
const connection = getConnection(data) || {
edges: [],
pageInfo: {
totalCount: 0,
hasNextPage: false,
hasPreviousPage: false,
},
};
const edges = connection.edges;
const nodes = edges.map((edge) => edge.node);
const firstCursor = edges[0]?.cursor;
const lastCursor = edges[edges.length - 1]?.cursor;
const loading = isLoading(networkStatus);
const previousPageLink = React.useMemo(() => {
if (!firstCursor) return "";
const params = new URLSearchParams(queryParams.toString());
params.set("cursor", firstCursor);
params.set("beforeOrAfter", "before");
params.set("firstOrLast", "last");
return `${baseUrl}?${params.toString()}`;
}, [queryParams, search, data]);
const nextPageLink = React.useMemo(() => {
if (!lastCursor) return "";
const params = new URLSearchParams(queryParams.toString());
params.set("cursor", lastCursor);
params.set("beforeOrAfter", "after");
params.set("firstOrLast", "first");
return `${baseUrl}?${params.toString()}`;
}, [queryParams, search, data]);
return {
loading,
nodes,
totalCount: connection.pageInfo.totalCount,
hasNextPage: connection.pageInfo.hasNextPage,
hasPreviousPage: connection.pageInfo.hasPreviousPage,
nextPageLink,
previousPageLink,
nextPage: () => {
queryParams.set("cursor", lastCursor);
queryParams.set("beforeOrAfter", "after");
queryParams.set("firstOrLast", "first");
navigate(`${baseUrl}?${queryParams.toString()}`, { replace: true });
},
previousPage: () => {
queryParams.set("cursor", firstCursor);
queryParams.set("beforeOrAfter", "before");
queryParams.set("firstOrLast", "last");
navigate(`${baseUrl}?${queryParams.toString()}`, { replace: true });
},
} as any;
}
function isLoading(status: NetworkStatus): boolean {
switch (status) {
case NetworkStatus.loading:
return true;
case NetworkStatus.fetchMore:
return true;
case NetworkStatus.setVariables:
return true;
}
return false;
}