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 tqa
This 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 GET
s. 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 inTPayload
will 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
useInfiniteQuery
hook. The configuration for the React Query part remans the same except for theinitialPageParam
property, 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);