Files
nextjs-fetch-data-example/ai/nextjs-style-guide/applied/rest-client/setup/hooks.md

314 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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