10 KiB
title, description, keywords
| title | description | keywords | |||||
|---|---|---|---|---|---|---|---|
| GET-хуки REST-клиента | Прозрачные SWR-обёртки над GET-методами REST-клиента. |
|
GET-хуки REST-клиента
Прозрачные SWR-обёртки над GET-методами REST-клиента.
Зачем нужны
GET-хуки нужны, чтобы Client Components получали REST-данные через SWR, но не работали с useSWR, ключами кеша и fetcher напрямую.
Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
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-хука всегда создаётся отдельной экспортируемой функцией.
Формат ключа:
['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 без имени сервиса.
Примеры ключей:
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-api', `/pet/${params.petId}`] as const
}
export const getPetListKey = (params?: FindPetsByStatusParams | null) => {
if (!params?.status) {
return null
}
return ['pet-store-api', `/pet/findByStatus?status=${params.status}`] as const
}
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 явно.
Пример списка
// 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-запроса
// 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)
}
Пример без параметров
// 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 для обязательных параметров. Это означает, что параметры ещё не готовы и запрос выполнять нельзя.
const key = getPetDetailKey(params)
Если params не готов, key-функция вернёт null. SWR не вызовет fetcher для null-ключа.
Не добавляйте отдельные isReady, throw new Error(...) и условный вызов useSWR.
Экспорт
// 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'
// 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
// Хорошо: infra, прозрачный GET-хук
const { data: pets } = useGetPetList({ status: StatusEnum.Available })
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
const query = useGetPetList({ status: StatusEnum.Available })
return {
...query,
hasPets: Boolean(query.data?.length),
}
}
hasPets — не часть GET-запроса, поэтому он не добавляется в useGetPetList.
Что запрещено
// Плохо — 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-хук.