--- 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`. - Для GET-метода без параметров хук принимает только `config?: SWRConfiguration`. - 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, ) => { const key = getPetListKey(params) const fetcher = () => petStoreApi.pet.findPetsByStatus( params as FindPetsByStatusParams, ) return useSWR(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, ) => { const key = getPetDetailKey(params) const fetcher = () => petStoreApi.pet.getPetById(params as GetPetByIdParams) return useSWR(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, ) => { return useSWR( getStoreInventoryKey(), () => petStoreApi.store.getInventory(), config, ) } ``` Если generated-метод возвращает безымянный тип вроде `Record`, а тип нужен наружу, вынесите его в `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(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).