A strongly typed hooks library based on TanStack's React Query & Axios to perform common CRUD operations for dead simple RESTful HTTP requests.
- TanStack's React Query 5+
- Axios 1.6+
npm i tqaThis library requires a default Axios instance in order to work. To provide said instance, wrap your application within ConsumerProvider, making sure it's already wrapped in TanStack's React Query's QueryClientProvider.
Example with Next.js and App Router:
"use client";
import axios from "axios";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Consumer, ConsumerProvider } from "tqa";
import type { ReactNode } from "react";
export interface ProvidersProps {
children?: ReactNode;
}
const http = axios.create();
const queryClient = new QueryClient();
const consumer = new Consumer(http);
export default function Providers({ children }: ProvidersProps) {
return (
<QueryClientProvider client={queryClient}>
<ConsumerProvider consumer={consumer}>{children}</ConsumerProvider>
</QueryClientProvider>
);
}This library represents my opinionated way of handling and standardizing common REST operations as much as possible, including different pagination strategies.
Every single query or mutation declared by mean of tqa will require a key. This is a personal preference of mine.
Since the whole query or mutation is completely handled by the hook of choice, it won't be possible to extend, override or customize the queryFn/mutationFn callback besides what's allowed by parameters and types. The rest of the hook's configuration will be left completely untouched so that it can be 100% handled by the library user.
Every Axios instance is referenced as "consumer". Queries and mutations are only performed with Axios, which will always require an endpoint, either as URL instance or plain string.
No default configuration is provided to Axios out of the box - it can be fully handled by the library user.
Each hook can be strongly typed to fit your needs, from the server's response to the URL parameters. All the hooks accept the same generics:
| Generic | Description |
|---|---|
TResponse |
The type definition for the response from the server |
TParams |
The type definition for a subset of URL parameters for Axios. Each field is recursively marked as optional |
TError |
The type definition for the error response from the server |
Hooks that allow for different request methods accept an additional generic:
| Generic | Description |
|---|---|
TRequest |
The request's nature. This will enforce the request's method and adjust the hook's configuration |
Hooks designed for requests with a payload accept an additional generic:
| Generic | Description |
|---|---|
TPayload |
The type definition for the request's body. The passed type's field are recursively marked as optional for PATCH requests |
When the query/mutation fails, the error is raised by Axios:
| Source | Type |
|---|---|
| Queries | Error | AxiosError<TError, void> |
| Mutations | Error | AxiosError<TError, TPayload> |
The data object exposed by each hook is what React Query returns from the fetcher/mutator.
Powered by Axios, it's enriched with more information:
| Attribute | Type | Description |
|---|---|---|
response |
TResponse |
The response returned by the server |
headers |
AxiosResponseHeaders | RawAxiosResponseHeaders |
The response's headers |
status |
number |
The response's status code |
statusText |
string |
The response's status message returned by the server |
Hooks receive separate configuration objects for TanStack's React Query and Axios. Each configuration's type adjusts according to the nature of the request you want to perform.
Basic example:
interface BookRetrieve {
id: number;
title: string;
created_at: string;
}
const bookId = 123;
const query = useRetrieve<"retrieve", BookRetrieve>(`/v1/book/${bookId}`, {
reactQuery: { queryKey: ["book", bookId] },
axios: { method: "get" },
}
);In addition, every hook accepts an optional consumer configuration object for when you want to perform requests with a completely different consumer without touching the default one you set in the provider. The configuration allows you to either switch to a generic, untouched instance:
const query = useRetrieve("...", {
/* ... */
consumer: {
external: true,
options: { /* ... */ },
},
}
);Or pass your own:
const myConsumer = axios.create({ /* ... */ });
const query = useRetrieve("...", {
/* ... */
consumer: {
instance: myConsumer,
options: { /* ... */ },
},
}
);When you need access to your global consumer from within React's components tree or hooks you can use useConsumer:
const consumer = useConsumer();This will allow you to read the current default values and edit the consumer instance, although accessing its raw Axios instance would suffice for the latter case.
Consumer configuration:
| Field | Type | Description |
|---|---|---|
mergeOptions? |
boolean | undefined |
Whether to merge the passed options with the default ones. Defaults to false |
options?.paginator?.itemsPerPage? |
number | undefined |
The number of records per page. Defaults to 10 |
options?.paginator?.limitParam? |
string | undefined |
The name of the limit parameter. Defaults to "limit" |
options?.paginator?.offsetParam? |
string | undefined |
The name of the offset parameter. Defaults to "offset" |
options?.paginator?.sendZeroOffset? |
boolean | undefined |
Whether to include the offset parameter in the URL when the value is 0. Defaults to false |
options?.paginator?.initialPageParam? |
number | undefined |
The initial offset parameter's value. Defaults to 0 |
The useRetrieve hook can be used to perform GET and HEAD requests.
import { useRetrieve } from "tqa/hooks/crud";
const query = useRetrieve<"retrieve" | "status", TResponse, TParams, TError>(url, config);The useRetrievalCreate hook is an alternate version of useRetrieve built specifically for performing POST requests behaving as GETs. Everything works as it does for useRetrieve with the addition of the payload's generic type TPayload and no need to specify TRequest.
import { useRetrievalCreate } from "tqa/hooks/crud/alt";
const query = useRetrievalCreate<TResponse, TPayload, TParams, TError>(url, config);The useCreationalRetrieve hook is an alternate version of useRetrieve built specifically for performing GET or HEAD with mutation dynamics. Everything works as it does for useRetrieve at declaration level.
import { useCreationalRetrieve } from "tqa/hooks/crud/alt";
const mutation = useCreationalRetrieve<"retrieve" | "status", TResponse, TParams, TError>(url, config);The useCreateUpdate hook can be used to perform POST, PUT and PATCH requests.
import { useCreateUpdate } from "tqa/hooks/crud";
const mutation = useCreateUpdate<"create" | "update" | "partialUpdate", TResponse, TPayload, TParams, TError>(url, config);For
"partialUpdate"requests, each field inTPayloadwill be automatically and recursively marked as optional.
The useDestroy hook can be used to perform DELETE requests only.
import { useDestroy } from "tqa/hooks/crud";
const mutation = useDestroy<TResponse, TParams, TError>(url, config);The pagination hooks are built on top of the assumption that you will be interacting with a server that adopts the limit/offset approach rather than the more traditional page/size one.
Pagination hooks can be configured like the regular CRUD ones, with the addition of specific fields and callbacks that can be also used for templating. They accept the same generics like the other hooks, but keep in mind that TResponse is also responsible for defining your pagination structure. You can build custom wrappers to keep your code DRY as explained later.
You can customize one or more pagination default parameter(s) when you provide the Consumer instance.
Example with Next.js and App Router:
"use client";
import axios from "axios";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Consumer, ConsumerProvider } from "tqa";
import type { ReactNode } from "react";
interface ProvidersProps {
children?: ReactNode;
}
const http = axios.create();
const queryClient = new QueryClient();
const consumer = new Consumer(http, {
paginator: {
limitParam: "limit",
offsetParam: "offset",
itemsPerPage: 10,
initialPageParam: 0,
sendZeroOffset: false,
},
});
export default function Providers({ children }: ProvidersProps) {
return (
<QueryClientProvider client={queryClient}>
<ConsumerProvider consumer={consumer}>{children}</ConsumerProvider>
</QueryClientProvider>
);
}The values used in the example are also the default ones.
Each parameter can be individually set/overridden when declaring the hook of your choice.
The following paragraphs will use the following response type as example:
interface PaginationResponse<T = unknown> {
count: number;
previous: number | null;
next: number | null;
results: T[];
}tqa's "infinite" hooks rely on TanStack's React Query's
useInfiniteQueryhook. The configuration for the React Query part remans the same except for theinitialPageParamproperty, which is entirely handled by tqa.
The useInfiniteRetrieve hook is built for cumulative pagination ("load more" strategy).
import { useInfiniteRetrieve } from "tqa/hooks/pagination";
const infinite = useInfiniteRetrieve<PaginationResponse<TResponse>, TParams, TError>(url, config);Additional configuration:
| Field | Type | Description |
|---|---|---|
lookup.results |
(response: TResponse) => unknown[] | readonly unknown[] |
Retrieves every page's record(s) |
lookup.total |
(response: TResponse) => number |
Returns the field that indicates how many total records are available |
Along with what's returned by TanStack's React Query's useInfiniteQuery hook, provides additional fields:
| Field | Type | Description |
|---|---|---|
total.records |
number |
The amount of all the available records that can be paginated |
total.fetched |
number |
The cumulating amount of currently fetched records |
If you want to standardize your pagination hook without having to pass the same types and configurations over and over, you can create a custom wrapper with the UseInfiniteRetrieveFactory type:
import { useInfiniteRetrieve, type UseInfiniteRetrieveFactory, type UseInfiniteRetrieveResult } from "tqa/hooks/pagination";
import type { Endpoint, Params } from "tqa/types";
import type { PaginationResponse } from "./types";
export const useInfinitePagination = <
TResponse = unknown,
TParams = Params,
TError = unknown
>(
url: Endpoint,
config: UseInfiniteRetrieveFactory<PaginationResponse<TResponse>, TParams, TError>
): UseInfiniteRetrieveResult<PaginationResponse<TResponse>, TError> =>
useInfiniteRetrieve<PaginationResponse<TResponse>, TParams, TError>(url, {
lookup: {
results: ({ results }) => results,
total: ({ count }) => count,
},
...config,
reactQuery: {
getNextPageParam: ({ response: { next } }) => next || undefined,
...config.reactQuery,
},
});The useInfiniteCreate hook is an alternate version of useInfiniteRetrieve built specifically for when the server you are querying to expects pagination through POST requests. Everything works as it does for useInfiniteRetrieve with the addition of the payload's generic type TPayload.
A custom wrapper can be built with the UseInfiniteCreateFactory type, which takes TPayload in addition.
import { useInfiniteCreate, type UseInfiniteCreateFactory, type UseInfiniteCreateResult } from "tqa/hooks/pagination/alt";
import type { Endpoint, Params } from "tqa/types";
import type { PaginationResponse } from "./types";
export const useInfinitePagination = <
TResponse = unknown,
TPayload = unknown,
TParams = Params,
TError = unknown
>(
url: Endpoint,
config: UseInfiniteCreateFactory<PaginationResponse<TResponse>, TPayload, TParams, TError>
): UseInfiniteCreateResult<PaginationResponse<TResponse>, TPayload, TError> =>
useInfiniteCreate<PaginationResponse<TResponse>, TPayload, TParams, TError>(url, config);The useDirectionalRetrieve hook is built for directional pagination ("previous"/"next" strategy).
import { useDirectionalRetrieve } from "tqa/hooks/pagination";
const query = useDirectionalRetrieve<PaginationResponse<TResponse>, TParams, TError>(url, config);Additional configuration:
| Field | Type | Description |
|---|---|---|
hasPreviousPage |
(response: TResponse, limit: number, offset: number) => boolean |
Determines whether there's a previous page |
hasNextPage |
(response: TResponse, limit: number, offset: number) => boolean |
Determines whether there's a next page |
getPreviousOffset |
(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number | undefined |
Retrieves the previous offset, if available |
getNextOffset |
(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number | undefined |
Retrieves the next offset, if available |
getIntervalFrom? |
(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number | undefined |
Determines the currently viewed interval's "from" index. If provided, getIntervalTo is mandatory |
getIntervalTo? |
(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number | undefined |
Determines the currently viewed interval's "to" index. If provided, getIntervalFrom is mandatory |
Along with what's returned by TanStack's React Query's useQuery hook, provides additional fields:
| Field | Type | Description |
|---|---|---|
hasPreviousPage |
boolean |
Whether the user can navigate backwards |
hasNextPage |
boolean |
Whether the user can navigate forward |
fetchPreviousPage |
() => void |
Fetches the previous page, if available |
fetchNextPage |
() => void |
Fetches the next page, if available |
interval |
{ from: number, to: number } | undefined |
Shows the currently viewed interval ("from"/"to" indexes) if configured |
setLimit |
Dispatch<SetStateAction<number>> |
Allows to manually set the current limit |
setOffset |
Dispatch<SetStateAction<number>> |
Allows to manually set the current offset |
resetOffset |
() => void |
Allows to reset the offset to the given initial page parameter |
The navigation control functions will take care of checking if the navigation can be performed by first checking if the data is still being retrieved, or if the page that is being requested is actually available, so that you don't have to.
If you want to standardize your pagination hook without having to pass the same types and configurations over and over, you can create a custom wrapper with the UseDirectionalRetrieveFactory type:
import { keepPreviousData } from "@tanstack/react-query";
import { useDirectionalRetrieve, type UseDirectionalRetrieveFactory, type UseDirectionalRetrieveResult } from "tqa/hooks/pagination";
import type { Endpoint, Params } from "tqa/types";
import type { PaginationResponse } from "./types";
export const useDirectionalPagination = <
TResponse = unknown,
TParams = Params,
TError = unknown
>(
url: Endpoint,
config: UseDirectionalRetrieveFactory<PaginationResponse<TResponse>, TParams, TError>
): UseDirectionalRetrieveResult<PaginationResponse<TResponse>, TError> =>
useDirectionalRetrieve<PaginationResponse<TResponse>, TParams, TError>(url, {
hasPreviousPage: ({ previous }) => typeof previous === "number",
hasNextPage: ({ next }) => typeof next === "number",
getPreviousOffset: ({ previous }) => previous ?? undefined,
getNextOffset: ({ next }) => next || undefined,
// Optional
getIntervalFrom: ({ results }, _, offset) => results.length ? offset + 1 : 0,
getIntervalTo: ({ results }, _, offset) => results.length + offset,
...config,
// Recommended
reactQuery: { placeholderData: keepPreviousData, ...config.reactQuery },
});The useDirectionalCreateCreate hook is an alternate version of useDirectionalCreateRetrieve built specifically for when the server you are querying to expects pagination through POST requests. Everything works as it does for useDirectionalCreateRetrieve with the addition of the payload's generic type TPayload.
A custom wrapper can be built with the UseDirectionalCreateFactory type, which takes TPayload in addition.
import { useDirectionalCreate, type UseDirectionalCreateFactory, type UseDirectionalCreateResult } from "tqa/hooks/pagination/alt";
import type { Endpoint, Params } from "tqa/types";
import type { PaginationResponse } from "./types";
export const useDirectionalPagination = <
TResponse = unknown,
TPayload = unknown,
TParams = Params,
TError = unknown
>(
url: Endpoint,
config: UseDirectionalCreateFactory<PaginationResponse<TResponse>, TPayload, TParams, TError>
): UseDirectionalCreateResult<PaginationResponse<TResponse>, TPayload, TError> =>
useDirectionalCreate<PaginationResponse<TResponse>, TPayload, TParams, TError>(url, config);