Files

10 KiB
Raw Permalink Blame History

title, description, keywords
title description keywords
GET-хуки REST-клиента Прозрачные SWR-обёртки над GET-методами REST-клиента.
rest
swr
get-хуки
client components
infra

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-хук.