Skip to content

GET-хуки REST-клиента

GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с useSWR напрямую.

Где лежат

GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:

text
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-состояние.

Пример списка

ts
// 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.

Пример начальных данных для клиентского хука:

tsx
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 и продолжает вызывать обычный хук:

tsx
const { data: pets } = useGetPetList('available')

Пример detail-запроса

ts
// 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.

ts
const isReady = id !== null
const key = isReady ? getPetDetailKey(id) : null

null не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через SWRConfig fallback.

Для числовых идентификаторов не используйте проверку if (id): значение 0 тоже валидное число. Проверяйте явно: id !== null.

Экспорт

ts
// 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'
ts
// src/infrastructure/pet-store-api/index.ts
export { petStoreApi } from './client'
export type { Pet } from './generated/pet-store-api.generated'
export * from './hooks'

Где заканчивается infrastructure

ts
// Хорошо: infrastructure, прозрачный GET-хук
const { data: pets } = useGetPetList('available')
ts
// Хорошо: business, доменная интерпретация
export const useAvailablePets = () => {
  const query = useGetPetList('available')

  return {
    ...query,
    hasPets: Boolean(query.data?.length),
  }
}

hasPets — не часть GET-запроса, поэтому он не добавляется в useGetPetList.

Что запрещено

ts
// Плохо — 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-хук.