# TanStack Query and Mutation templates for React RSC/SSR/RCC

*Recommendations on how to structure TanStack queries and mutations. Provided templates define hooks with extensible interfaces, supporting custom fetch functions per request. RSC/SSR/RCC compatible.*

## Expectations

* Compatible with some **RSC/SSR** frameworks - **fetch requests should exist independently**.
    
* Good **invalidation possibilities** - prefix and predicate statements defined in one place.
    
* [**Axios**](https://github.com/axios/axios) library (with a simple replacement with native fetch, e.g. in Next.js case).
    
* There is no way to generate hooks for Your project, e.g. via GraphQL [Codegen](https://the-guild.dev/graphql/codegen). See [TanStack Docs](https://tanstack.com/query/latest/docs/framework/react/community/community-projects).
    

## Project structure

* **Globally shared hooks**
    
    * One folder to rule them all: `~/src/queries` (or separate mutations into their own `~/src/mutations` folder).
        
    * It is somehow similar to the result of a code generator.
        
* **Component-based** hooks
    
    * The main concept is high cohesion - hooks are placed according to usage as near as possible to the components that import them.
        
    * For more details, see [React folder structure](https://notes.dunaevskiy.dev/react-folder-structure-recommendations-based-on-nextjs-approuter).
        

Each file is named according to its react hook and exports all types, fetch requests and the hook itself (or query option). For example, we have a REST endpoint `GET /api/v1/users`. Method `GET` and unique entity name `Users` are included in the final hook name.

<center><h3>useQuery(HttpMethod)(Entity).tsx</h3><h3>useQueryGetUsers.tsx</h3></center>

The same naming convention is applied to mutations:

<center><h3>useMutation(HttpMethod)(Entity).tsx</h3><h3>useMutationPatchUser.tsx</h3></center>

We also need to manage and store query keys and invalidation groups in case of queries. **Never define them inside hooks.** The whole set of keys EOT will become a mess in this case. Query keys are similar to primary keys in databases, so it is good to normalize them. See database normal forms - [1NF, 2NF, and 3NF](https://www.geeksforgeeks.org/normal-forms-in-dbms/) (normalization is not required, but it helps a lot).

## Query keys and invalidation groups

It is a good idea to define one file that will manage all query keys across an app.

```typescript
// ~/src/configs/tsq.ts

import type { Query, QueryKey } from '@tanstack/react-query';

type Filters = { [key: string]: string | number | undefined };
type QueryKeyList = { [key: string]: (...args: never[]) => QueryKey };
type QueryKeyPredicate = (query: Query) => boolean;
type QueryKeyInvalidationList = {
  [key: string]: QueryKey | QueryKeyPredicate | ((...args: never[]) => QueryKeyPredicate);
};

/**
 * List of QueryKeys
 */
export const QK = {
  // Examples
  // getSomeEntity:                  ()                                => ['group', 'key'],
  // getSomeEntityWithFilter:        (filters: Filters)                => ['group', 'key', filters],
} as const satisfies QueryKeyList;

/**
 * Query invalidators (by prefix and by predicate)
 * See https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation
 */
export const QK_INVALIDATION = {
  // Examples
  // prefixKeyFromQK:   QK.getSomeEntity(),
  // prefixKeyCustom:   ['group'],
  // predicateKey:      (query: Query) => query.queryKey.includes('organizationContext'),
  // genPredicateKey:   (pageSize: number) => (query: Query) => (query.queryKey[1] as {[key: string]: unknown})?.pageSize === pageSize,
} as const satisfies QueryKeyInvalidationList;
```

## useQuery template

```typescript
// ~/src/queries/useQueryGetEntity.ts

import { keepPreviousData, queryOptions } from '@tanstack/react-query';
import { AxiosInstance } from 'axios';
import { QK } from '~/src/configs/tsq.ts';

/**
 * useQuery<MethodEntity>.ts
 *
 * <MethodEntity> | <methodEntity>
 * Choose a method from API: GET, POST, PUT, DELETE, PATCH, ... that represents action.
 * Choose an entity name: User, OrderList, Orders, InstitutionEnum, ... that represents a resource.
 * These names are used to create a name <MethodEntity> for the API endpoints: GetInstitutionEnum, PostUser, ...
 */

/**
 * <MethodEntity>RequestData (optional)
 * Request data - used for a request creation.
 */
export type GetEntityRequestData = {
  id: number;
  queryParams: {
    // Pagination
    page: number;
    limit: 10 | 25 | 50 | 100;
    // Filters
    search?: string;
  };
};

/**
 * <MethodEntity>ResponseData (required)
 * Response data - returned from the server (inside body).
 * This type should not be declared anywhere else and should not use any external type declarations (out of this file).
 * This file is a single source of truth that defines a behavior of the endpoint.
 */
export type GetEntityResponseData = {
  id: number;
  name: string;
  quantity: number;
};

/**
 * <methodEntity>Request (required)
 * Request - stand-alone request. For SSR and RSC pre-fetch purposes.
 * @param axios - Axios client. It is parametrized to allow SSR and RSC to use different Axios instances.
 * @param requestData - Static data used for request creation.
 * @returns Callable function for a request.
 */
export const getEntityRequest =
  ({ axios, requestData }: { axios: AxiosInstance; requestData: GetEntityRequestData }) =>
  () =>
    axios.get<GetEntityResponseData>(`/api/mock/users/${requestData.id}`).then((res) => res.data);

/**
 * <MethodEntity>QueryRequestData (optional)
 * Query options - used for query config creation (server prefetch and client fetch).
 */
export type GetEntityQueryRequestData = {
  requestData: GetEntityRequestData; // required if request is parametrized
};

/**
 * <methodEntity>QueryOptions (required)
 * Query options - used for query creation (server prefetch and client fetch).
 * @param axios - Axios client.
 * @param queryRequestData - Static data used for query creation. Includes request data.
 * @param select - Custom selector.
 */
export const getEntityQueryOptions = <T>({
  axios,
  queryRequestData,
  select,
}: {
  axios: AxiosInstance;
  queryRequestData: GetEntityQueryRequestData;
  select?: (data: GetEntityResponseData) => T;
}) =>
  queryOptions({
    queryKey: QK.enumProducts(),
    queryFn: getEntityRequest({ axios, requestData: queryRequestData.requestData }),
    placeholderData: keepPreviousData,
    select,
  });

// Custom selectors that can be used with query options.

export const selectGetEntityItemsQuantity = (data: GetEntityResponseData) => data.quantity;
```

## useMutation template

```typescript
// ~/src/mutations/useMutationPatchEntity.ts

import { useMutation } from '@tanstack/react-query';
import { AxiosInstance } from 'axios';

/**
 * useQuery<MethodEntity>.ts
 *
 * <MethodEntity> | <methodEntity>
 * Choose a method from API: GET, POST, PUT, DELETE, PATCH, ... that represents action.
 * Choose an entity name: User, OrderList, Orders, InstitutionEnum, ... that represents a resource.
 * These names are used to create a name <MethodEntity> for the API endpoints: GetInstitutionEnum, PostUser, ...
 */

/**
 * <MethodEntity>RequestStaticData (optional)
 * Request static data - used for a request creation. Data not changing with each request (e.g. ID of the entity).
 */
export type PatchEntityRequestStaticData = {
  id: number;
};

/**
 * <MethodEntity>RequestData (optional)
 * Request dynamic data - used for a request call (e.g. user input).
 */
export type PatchEntityRequestDynamicData = {
  name: string;
};

/**
 * <MethodEntity>ResponseData (required)
 * Response data - returned from the server (inside body).
 * This type should not be declared anywhere else and should not use any external type declarations (out of this file).
 * This file is a single source of truth that defines a behavior of the endpoint.
 */
export type PatchEntityResponseData = {
  id: number;
  name: string;
};

/**
 * <methodEntity>Request (required)
 * Request - stand-alone fetch.
 * @param axios - Axios client
 * @param requestStaticData - Request static data
 * @param () => requestDynamicData - Request dynamic data
 */
export const patchEntityRequest =
  ({
    axios,
    requestStaticData,
  }: {
    axios: AxiosInstance;
    requestStaticData: PatchEntityRequestStaticData;
  }) =>
  (requestDynamicData: PatchEntityRequestDynamicData) =>
    axios
      .patch<PatchEntityResponseData>(`/api/mock/users/${requestStaticData.id}`, requestDynamicData)
      .then((res) => res.data);

/**
 *
 * <methodEntity>MutationStaticData (optional)
 * Mutation static data - used for mutation and request creation.
 */
export type PatchEntityMutationRequestData = {
  requestStaticData: PatchEntityRequestStaticData;
};

/**
 * useMutation<MethodEntity> (required)
 * React hook - used for mutation creation.
 * @param axios - Axios client.
 * @param mutationStaticData - Static data used for mutation creation.
 */
export const useMutationPatchEntity = ({
  axios,
  mutationStaticData,
}: {
  axios: AxiosInstance;
  mutationStaticData: PatchEntityMutationRequestData;
}) =>
  useMutation({
    mutationFn: patchEntityRequest({
      axios,
      requestStaticData: mutationStaticData.requestStaticData,
    }),
  });
```

## Usage

Both templates are almost agnostic and should be suited for each request. Let's say that we have an endpoint for user private projects:

`GET /api/v2/users/project?type=private`

Our hook is called `useQueryUserProjects` to make it universal because of the possibility of some public projects, etc. We need static data with a project type, a response type is generated from OpenAPI definition, and there is no need to configure fetch request with each hook call (only request itself, because of server calls):

```typescript
// ~/src/configs/tsq.ts

import type { Query, QueryKey } from '@tanstack/react-query';

type Filters = { [key: string]: string | number | undefined };
type QueryKeyList = { [key: string]: (...args: never[]) => QueryKey };
type QueryKeyPredicate = (query: Query) => boolean;
type QueryKeyInvalidationList = {
  [key: string]: QueryKey | QueryKeyPredicate | ((...args: never[]) => QueryKeyPredicate);
};

export const QK = {
  // ...
  getUserProjects:  (filters: Filters) => ['users', 'projects', filters],
} as const satisfies QueryKeyList;

// optional
export const QK_INVALIDATION = {
  prefixUsers: ['users'],
  prefixUserProjects: ['users', 'projects'],
} as const satisfies QueryKeyInvalidationList;
```

```typescript
// ~/src/queries/useQueryGetUserProjects.ts

import { keepPreviousData, queryOptions } from '@tanstack/react-query';
import { AxiosInstance } from 'axios';
import ApiTypes from '~/swagger-types';
import { QK } from '~/src/configs/tsq'

export type GetUserProjectsRequestData = {
	type?: string;
};

export type GetUserProjectsResponseData =
	ApiTypes.paths['/api/v2/users/project']['get']['responses']['200']['content']['application/json'];

export const getUserProjectsRequest =
	({ axios, requestData }: { axios: AxiosInstance; requestData: GetUserProjectsRequestData }) =>
	() =>
		axios
			.get<GetUserProjectsResponseData>('/api/v2/users/project', {
				params: {
					type: requestData.type,
				},
			})
			.then((res) => res.data);

export type GetUserProjectsQueryRequestData = {
	requestData: GetUserProjectsRequestData;
};

export const getUserProjectsQueryOptions = <T>({
  axios,
  queryRequestData,
  select,
}: {
  axios: AxiosInstance;
  queryRequestData: GetUserProjectsQueryRequestData;
  select?: (data: GetUserProjectsResponseData) => T;
}) =>
  queryOptions({
    queryKey: QK.getUserProjects({ type: queryRequestData.requestData.type }), 
    queryFn: getUserProjectsRequest({ axios, requestData: queryRequestData.requestData }),
    placeholderData: keepPreviousData,
    select,
  });
```

```typescript
// Component.tsx

import axiosClient = '~/axios';
import { getUserProjectsQueryOptions } from '~/src/queries/useQueryGetUserProjects.ts';

export const Component = () => {
  const { data: dataGetUserProjects, isFetching: isFetchingGetUserProjects } = useQuery(
    getUserProjectsQueryOptions({
      axios: axiosClient,
      queryRequestData: {
        requestData: {
          type: 'private'
        },
      },
    }),
  );

  return <>...</>;
};
```

The original interface is preserved - it has consistency, and it is easy to extend. At first, it may feel like there is a lot of code for just one simple request - yes, deal with it. It may be shorter, but then it is less extendable, and EOT each request is going to have its own special way of calling it.
