1.2.6 • Published 10 days ago

tqa v1.2.6

Weekly downloads
-
License
MIT
Repository
github
Last release
10 days ago

tqa

A strongly typed hooks library based on TanStack's React Query & Axios to perform common CRUD operations for dead simple RESTful HTTP requests.

Table of contents

Requirements

  • TanStack's React Query 5+
  • Axios 1.6+

Install

npm i tqa

Quickstart

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>
  );
}

Fundamentals

This library represents my opinionated way of handling and standardizing common REST operations as much as possible, including different pagination strategies.

TanStack's React Query

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.

Axios

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.

TypeScript

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:

GenericDescription
TResponseThe type definition for the response from the server
TParamsThe type definition for a subset of URL parameters for Axios. Each field is recursively marked as optional
TErrorThe type definition for the error response from the server

Hooks that allow for different request methods accept an additional generic:

GenericDescription
TRequestThe 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:

GenericDescription
TPayloadThe 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:

SourceType
QueriesError \| AxiosError<TError, void>
MutationsError \| AxiosError<TError, TPayload>

Data

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:

AttributeTypeDescription
responseTResponseThe response returned by the server
headersAxiosResponseHeaders \| RawAxiosResponseHeadersThe response's headers
statusnumberThe response's status code
statusTextstringThe response's status message returned by the server

Hooks

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: { /* ... */ },
    },
  }
);

Consumer

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:

FieldTypeDescription
mergeOptions?boolean \| undefinedWhether to merge the passed options with the default ones. Defaults to false
options?.paginator?.itemsPerPage?number \| undefinedThe number of records per page. Defaults to 10
options?.paginator?.limitParam?string \| undefinedThe name of the limit parameter. Defaults to "limit"
options?.paginator?.offsetParam?string \| undefinedThe name of the offset parameter. Defaults to "offset"
options?.paginator?.sendZeroOffset?boolean \| undefinedWhether to include the offset parameter in the URL when the value is 0. Defaults to false
options?.paginator?.initialPageParam?number \| undefinedThe initial offset parameter's value. Defaults to 0

CRUD

Retrieve, status

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);
Retrieve (POST)

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);
Retrieve on demand

The useCreationalRetrieve hook is an alternate version of useRetrieve built specifically for performing GET with mutation dynamics. Everything works as it does for useRetrieve except the need to specify TRequest.

import { useCreationalRetrieve } from "tqa/hooks/crud/alt";

const mutation = useCreationalRetrieve<TResponse, TParams, TError>(url, config);

Create, update, partial update

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 in TPayload will be automatically and recursively marked as optional.

Destroy

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);

Pagination

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[];
}

Infinite

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 the initialPageParam 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:

FieldTypeDescription
lookup.results(response: TResponse) => unknown[] \| readonly unknown[]Retrieves every page's record(s)
lookup.total(response: TResponse) => numberReturns 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:

FieldTypeDescription
total.recordsnumberThe amount of all the available records that can be paginated
total.fetchednumberThe 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,
    },
  });

Infinite (POST)

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);

Directional

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:

FieldTypeDescription
hasPreviousPage(response: TResponse, limit: number, offset: number) => booleanDetermines whether there's a previous page
hasNextPage(response: TResponse, limit: number, offset: number) => booleanDetermines whether there's a next page
getPreviousOffset(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number \| undefinedRetrieves the previous offset, if available
getNextOffset(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number \| undefinedRetrieves the next offset, if available
getIntervalFrom?(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number \| undefinedDetermines the currently viewed interval's "from" index. If provided, getIntervalTo is mandatory
getIntervalTo?(response: TResponse, limit: number, offset: number, hasPrevPage: boolean, hasNextPage: boolean) => number \| undefinedDetermines 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:

FieldTypeDescription
hasPreviousPagebooleanWhether the user can navigate backwards
hasNextPagebooleanWhether the user can navigate forward
fetchPreviousPage() => voidFetches the previous page, if available
fetchNextPage() => voidFetches the next page, if available
interval{ from: number, to: number } \| undefinedShows the currently viewed interval ("from"/"to" indexes) if configured
setLimitDispatch<SetStateAction<number>>Allows to manually set the current limit
setOffsetDispatch<SetStateAction<number>>Allows to manually set the current offset
resetOffset() => voidAllows 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 },
  });

Directional (POST)

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);
1.2.6

10 days ago

1.2.5

10 days ago

1.2.4

10 days ago

1.2.3

10 days ago

1.2.2

11 days ago

1.2.0

27 days ago

1.2.1

27 days ago

1.1.9

27 days ago

1.1.8

27 days ago

1.1.7

27 days ago

1.1.6

27 days ago

1.1.5

27 days ago

1.1.4

27 days ago

1.1.3

27 days ago

1.1.2

1 month ago

1.1.1

2 months ago

1.1.0

2 months ago

1.0.9

2 months ago

1.0.8

2 months ago

1.0.7

3 months ago

1.0.6

4 months ago

1.0.5

4 months ago

1.0.4

4 months ago

1.0.3

4 months ago

1.0.2

4 months ago

1.0.1

4 months ago

1.0.0

4 months ago

0.9.9

4 months ago

0.8.8

5 months ago

0.8.7

5 months ago

0.8.6

5 months ago

0.9.0

5 months ago

0.9.2

5 months ago

0.9.1

5 months ago

0.9.8

5 months ago

0.9.7

5 months ago

0.9.4

5 months ago

0.9.3

5 months ago

0.9.6

5 months ago

0.9.5

5 months ago

0.8.5

5 months ago

0.8.4

5 months ago

0.8.1

5 months ago

0.8.3

5 months ago

0.8.2

5 months ago

0.7.4

5 months ago

0.7.9

5 months ago

0.7.6

5 months ago

0.7.5

5 months ago

0.7.8

5 months ago

0.7.7

5 months ago

0.8.0

5 months ago

0.6.7

5 months ago

0.6.6

5 months ago

0.6.9

5 months ago

0.6.8

5 months ago

0.7.2

5 months ago

0.6.3

5 months ago

0.7.1

5 months ago

0.6.2

5 months ago

0.6.5

5 months ago

0.7.3

5 months ago

0.6.4

5 months ago

0.7.0

5 months ago

0.6.1

5 months ago

0.5.8

5 months ago

0.4.9

5 months ago

0.5.7

5 months ago

0.4.8

5 months ago

0.5.9

5 months ago

0.5.4

5 months ago

0.4.5

5 months ago

0.5.3

5 months ago

0.4.4

5 months ago

0.5.6

5 months ago

0.4.7

5 months ago

0.5.5

5 months ago

0.4.6

5 months ago

0.5.0

5 months ago

0.4.1

5 months ago

0.4.0

5 months ago

0.5.2

5 months ago

0.4.3

5 months ago

0.6.0

5 months ago

0.5.1

5 months ago

0.4.2

5 months ago

0.3.9

5 months ago

0.3.6

5 months ago

0.3.5

5 months ago

0.3.8

5 months ago

0.3.7

5 months ago

0.3.4

5 months ago

0.3.3

5 months ago

0.3.2

5 months ago

0.3.1

5 months ago

0.3.0

5 months ago

0.2.0

5 months ago

0.1.0

5 months ago