forked from templates/nextjs-template
314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
|
|
---
|
|||
|
|
title: GET-хуки REST-клиента
|
|||
|
|
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
|||
|
|
keywords: [rest, swr, get-хуки, client components, infra]
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# GET-хуки REST-клиента
|
|||
|
|
|
|||
|
|
Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
|||
|
|
|
|||
|
|
## Зачем нужны
|
|||
|
|
|
|||
|
|
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с `useSWR`, ключами кеша и fetcher напрямую.
|
|||
|
|
|
|||
|
|
## Где лежат
|
|||
|
|
|
|||
|
|
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
src/infra/
|
|||
|
|
└── pet-store-api/
|
|||
|
|
├── client.ts
|
|||
|
|
├── generated/
|
|||
|
|
├── hooks/
|
|||
|
|
│ ├── use-get-pet-list.hook.ts
|
|||
|
|
│ ├── use-get-pet-detail.hook.ts
|
|||
|
|
│ └── index.ts
|
|||
|
|
├── types/
|
|||
|
|
└── index.ts
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Контракт
|
|||
|
|
|
|||
|
|
- Один GET-хук = один GET-метод клиента.
|
|||
|
|
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
|
|||
|
|
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
|
|||
|
|
- Хук принимает `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
|
|||
|
|
- Для GET-метода без параметров хук принимает только `config?: SWRConfiguration<Data>`.
|
|||
|
|
- Key-функция принимает те же `params`, что и хук.
|
|||
|
|
- Key-функция возвращает `null`, если обязательные параметры не готовы.
|
|||
|
|
- Проверка готовности запроса живёт в key-функции, а не в теле хука.
|
|||
|
|
- Хук вызывает `useSWR` один раз и безусловно.
|
|||
|
|
- Fetcher не проверяет `null`, не бросает ошибку и не вызывает метод клиента с `null`.
|
|||
|
|
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
|
|||
|
|
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
|
|||
|
|
- Хук не объединяет несколько запросов.
|
|||
|
|
- Хук не маппит DTO в доменную модель.
|
|||
|
|
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
|||
|
|
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
|
|||
|
|
|
|||
|
|
## Формат SWR-ключа
|
|||
|
|
|
|||
|
|
SWR-ключ GET-хука всегда создаётся отдельной экспортируемой функцией.
|
|||
|
|
|
|||
|
|
Формат ключа:
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
['pet-store-api', '/pet/10'] as const
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Первый элемент — имя API-сервиса или REST-клиента в `kebab-case`.
|
|||
|
|
- Второй элемент — endpoint запроса: path и query string.
|
|||
|
|
- Key-функция возвращает `null`, когда запрос нельзя выполнять.
|
|||
|
|
- Key-функция нужна и GET-хуку, и `SWRConfig fallback`.
|
|||
|
|
- Не используйте произвольные части вроде `['pet-store-api', 'pet', 'detail', params]`.
|
|||
|
|
- Не используйте только строку endpoint без имени сервиса.
|
|||
|
|
|
|||
|
|
Примеры ключей:
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
|||
|
|
if (!params?.petId) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ['pet-store-api', `/pet/${params.petId}`] as const
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
|||
|
|
if (!params?.status) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
export const getPetListByTagsKey = (params?: FindPetsByTagsParams | null) => {
|
|||
|
|
if (!params?.tags.length) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ['pet-store-api', `/pet/findByTags?tags=${params.tags.join(',')}`] as const
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Если API допускает `0` как валидный идентификатор, не используйте проверку `!params?.id`. В таком случае проверяйте `null` и `undefined` явно.
|
|||
|
|
|
|||
|
|
## Пример списка
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/infra/pet-store-api/hooks/use-get-pet-list.hook.ts
|
|||
|
|
import type { SWRConfiguration } from 'swr'
|
|||
|
|
import useSWR from 'swr'
|
|||
|
|
import { petStoreApi } from '../client'
|
|||
|
|
import type {
|
|||
|
|
FindPetsByStatusParams,
|
|||
|
|
Pet,
|
|||
|
|
} from '../generated/pet-store-api.generated'
|
|||
|
|
|
|||
|
|
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
|
|||
|
|
if (!params?.status) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Получает список питомцев по статусу.
|
|||
|
|
*/
|
|||
|
|
export const useGetPetList = (
|
|||
|
|
params?: FindPetsByStatusParams | null,
|
|||
|
|
config?: SWRConfiguration<Pet[]>,
|
|||
|
|
) => {
|
|||
|
|
const key = getPetListKey(params)
|
|||
|
|
const fetcher = () => petStoreApi.pet.findPetsByStatus(
|
|||
|
|
params as FindPetsByStatusParams,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return useSWR<Pet[]>(key, fetcher, config)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`params as FindPetsByStatusParams` допустим только в fetcher: готовность параметров проверена в key-функции, а при `key = null` SWR не вызывает fetcher.
|
|||
|
|
|
|||
|
|
## Пример detail-запроса
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/infra/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
|||
|
|
import type { SWRConfiguration } from 'swr'
|
|||
|
|
import useSWR from 'swr'
|
|||
|
|
import { petStoreApi } from '../client'
|
|||
|
|
import type { GetPetByIdParams, Pet } from '../generated/pet-store-api.generated'
|
|||
|
|
|
|||
|
|
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
|
|||
|
|
if (!params?.petId) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ['pet-store-api', `/pet/${params.petId}`] as const
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Получает детальную карточку питомца с кешированием результата.
|
|||
|
|
*/
|
|||
|
|
export const useGetPetDetail = (
|
|||
|
|
params?: GetPetByIdParams | null,
|
|||
|
|
config?: SWRConfiguration<Pet>,
|
|||
|
|
) => {
|
|||
|
|
const key = getPetDetailKey(params)
|
|||
|
|
const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams)
|
|||
|
|
|
|||
|
|
return useSWR<Pet>(key, fetcher, config)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Пример без параметров
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/infra/pet-store-api/hooks/use-get-store-inventory.hook.ts
|
|||
|
|
import type { SWRConfiguration } from 'swr'
|
|||
|
|
import useSWR from 'swr'
|
|||
|
|
import { petStoreApi } from '../client'
|
|||
|
|
import type { StoreInventory } from '../types'
|
|||
|
|
|
|||
|
|
export const getStoreInventoryKey = () => {
|
|||
|
|
return ['pet-store-api', '/store/inventory'] as const
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Получает инвентарь магазина.
|
|||
|
|
*/
|
|||
|
|
export const useGetStoreInventory = (
|
|||
|
|
config?: SWRConfiguration<StoreInventory>,
|
|||
|
|
) => {
|
|||
|
|
return useSWR<StoreInventory>(
|
|||
|
|
getStoreInventoryKey(),
|
|||
|
|
() => petStoreApi.store.getInventory(),
|
|||
|
|
config,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Если generated-метод возвращает безымянный тип вроде `Record<string, number>`, а тип нужен наружу, вынесите его в `types/`.
|
|||
|
|
|
|||
|
|
## Отложенный запрос
|
|||
|
|
|
|||
|
|
GET-хук может принимать `null` или `undefined` для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
const key = getPetDetailKey(params)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Если `params` не готов, key-функция вернёт `null`. SWR не вызовет fetcher для `null`-ключа.
|
|||
|
|
|
|||
|
|
Не добавляйте отдельные `isReady`, `throw new Error(...)` и условный вызов `useSWR`.
|
|||
|
|
|
|||
|
|
## Экспорт
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/infra/pet-store-api/hooks/index.ts
|
|||
|
|
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
|
|||
|
|
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
|
|||
|
|
export {
|
|||
|
|
getStoreInventoryKey,
|
|||
|
|
useGetStoreInventory,
|
|||
|
|
} from './use-get-store-inventory.hook'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/infra/pet-store-api/index.ts
|
|||
|
|
export { petStoreApi } from './client'
|
|||
|
|
export type {
|
|||
|
|
FindPetsByStatusParams,
|
|||
|
|
GetPetByIdParams,
|
|||
|
|
Pet,
|
|||
|
|
} from './generated/pet-store-api.generated'
|
|||
|
|
export { PetStatusEnum, StatusEnum } from './generated/pet-store-api.generated'
|
|||
|
|
export * from './hooks'
|
|||
|
|
export type { StoreInventory } from './types'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Наружу импортируют только из `infra/pet-store-api`, не из `generated/` и не из `hooks/` напрямую.
|
|||
|
|
|
|||
|
|
## Где заканчивается infra
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// Хорошо: infra, прозрачный GET-хук
|
|||
|
|
const { data: pets } = useGetPetList({ status: StatusEnum.Available })
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// Хорошо: business, доменная интерпретация
|
|||
|
|
export const useAvailablePets = () => {
|
|||
|
|
const query = useGetPetList({ status: StatusEnum.Available })
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
...query,
|
|||
|
|
hasPets: Boolean(query.data?.length),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
|||
|
|
|
|||
|
|
## Что запрещено
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// Плохо — useSWR в компоненте
|
|||
|
|
const { data } = useSWR(
|
|||
|
|
['pet-store-api', '/pet/findByStatus?status=available'],
|
|||
|
|
() => petStoreApi.pet.findPetsByStatus({ status: StatusEnum.Available }),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Плохо — проверка готовности размазана по хуку
|
|||
|
|
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
|
|||
|
|
const key = params?.petId ? getPetDetailKey(params) : null
|
|||
|
|
const fetcher = () => {
|
|||
|
|
if (!params?.petId) {
|
|||
|
|
throw new Error('Pet id is required')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return petStoreApi.pet.getPetById(params)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return useSWR<Pet>(key, fetcher)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Плохо — условный вызов useSWR нарушает rules of hooks
|
|||
|
|
export const useGetPetDetail = (params?: GetPetByIdParams | null) => {
|
|||
|
|
const key = getPetDetailKey(params)
|
|||
|
|
|
|||
|
|
if (key === null) {
|
|||
|
|
return useSWR(null, null)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return useSWR(key, () => petStoreApi.pet.getPetById(params))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Плохо — несколько GET внутри infra-хука
|
|||
|
|
export const usePetDashboard = () => {
|
|||
|
|
const available = useGetPetList({ status: StatusEnum.Available })
|
|||
|
|
const sold = useGetPetList({ status: StatusEnum.Sold })
|
|||
|
|
|
|||
|
|
return { available, sold }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
|||
|
|
export const useGetPetList = (params?: FindPetsByStatusParams | null) => {
|
|||
|
|
const query = useSWR(...)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
...query,
|
|||
|
|
hasPets: Boolean(query.data?.length),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](../../data-fetch/client-get-hook.md).
|