GET-хуки REST-клиента
GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с useSWR напрямую.
Где лежат
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
src/infrastructure/
└── pet-store-api/
├── client.ts
├── generated/
├── hooks/
│ ├── use-get-pet-list.hook.ts
│ ├── use-get-pet-detail.hook.ts
│ └── index.ts
└── index.tsКонтракт
- Один GET-хук = один GET-метод клиента.
- Имя GET-хука начинается с
useGet:useGetPetList,useGetPetDetail. - Имя файла начинается с
use-get:use-get-pet-list.hook.ts. - Хук принимает только параметры GET-метода и
config?: SWRConfiguration. - Что передали хуку, то он передаёт в GET-метод.
- Внутри только SWR-механика: key, fetcher,
useSWR,config. - Хук возвращает тип ответа API: generated-тип или DTO из
types/. - Хук не объединяет несколько запросов.
- Хук не маппит DTO в доменную модель.
- Хук не вычисляет бизнес-флаги:
isAuth,canEdit,hasAccess,hasPets. - Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
Пример списка
// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petStoreApi } from '../client'
import type { Pet } from '../generated/pet-store-api.generated'
export type PetStatus = 'available' | 'pending' | 'sold'
export const getPetListKey = (status: PetStatus) =>
['pet-store-api', 'pet', 'list', status] as const
/**
* Получение списка питомцев по статусу.
*/
export const useGetPetList = (status: PetStatus | null, config?: SWRConfiguration) => {
const isReady = status !== null
const key = isReady ? getPetListKey(status) : null
const fetcher = () => petStoreApi.pet.findPetsByStatus({ status })
return useSWR<Pet[]>(key, fetcher, config)
}Функция getPetListKey нужна, чтобы один и тот же SWR-ключ использовался внутри GET-хука и при передаче начальных данных через SWRConfig fallback.
Пример начальных данных для клиентского хука:
import type { ReactNode } from 'react'
import { SWRConfig, unstable_serialize } from 'swr'
import {
getPetListKey,
petStoreApi,
} from 'infrastructure/pet-store-api'
export default function PetsLayout({ children }: { children: ReactNode }) {
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
return (
<SWRConfig
value={{
fallback: {
[unstable_serialize(getPetListKey('available'))]: petsPromise,
},
}}
>
{children}
</SWRConfig>
)
}Клиентский компонент при этом ничего не знает про preload/fallback и продолжает вызывать обычный хук:
const { data: pets } = useGetPetList('available')Пример detail-запроса
// src/infrastructure/pet-store-api/hooks/use-get-pet-detail.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petStoreApi } from '../client'
import type { Pet } from '../generated/pet-store-api.generated'
export const getPetDetailKey = (id: number) =>
['pet-store-api', 'pet', 'detail', id] as const
/**
* Получение питомца по идентификатору.
*/
export const useGetPetDetail = (id: number | null, config?: SWRConfiguration) => {
const isReady = id !== null
const key = isReady ? getPetDetailKey(id) : null
const fetcher = () => petStoreApi.pet.getPetById(id)
return useSWR<Pet>(key, fetcher, config)
}Отложенный запрос через null
GET-хук может принимать null для обязательного параметра. null означает, что параметр ещё не готов и запрос выполнять нельзя.
Внутри хука это выражается через isReady: если параметр не готов, ключ SWR становится null, и SWR не вызывает fetcher.
const isReady = id !== null
const key = isReady ? getPetDetailKey(id) : nullnull не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через SWRConfig fallback.
Для числовых идентификаторов не используйте проверку if (id): значение 0 тоже валидное число. Проверяйте явно: id !== null.
Экспорт
// src/infrastructure/pet-store-api/hooks/index.ts
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
export type { PetStatus } from './use-get-pet-list.hook'
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'// src/infrastructure/pet-store-api/index.ts
export { petStoreApi } from './client'
export type { Pet } from './generated/pet-store-api.generated'
export * from './hooks'Где заканчивается infrastructure
// Хорошо: infrastructure, прозрачный GET-хук
const { data: pets } = useGetPetList('available')// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
const query = useGetPetList('available')
return {
...query,
hasPets: Boolean(query.data?.length),
}
}hasPets — не часть GET-запроса, поэтому он не добавляется в useGetPetList.
Что запрещено
// Плохо — useSWR в компоненте
const { data } = useSWR(
['pet-store-api', 'pet', 'list', status],
() => petStoreApi.pet.findPetsByStatus({ status }),
)
// Плохо — несколько GET внутри infrastructure-хука
export const usePetDashboard = () => {
const available = useGetPetList('available')
const sold = useGetPetList('sold')
return { available, sold }
}
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
export const useGetPetList = (status: PetStatus) => {
const query = useSWR(...)
return {
...query,
hasPets: Boolean(query.data?.length),
}
}Подробное потребление таких хуков описано в стратегии Клиентский GET-хук.