Кастомные хуки

TanStack Query в JavaScript строится вокруг идеи инкапсуляции серверного состояния и его жизненного цикла. Базовые хуки вроде useQuery и useMutation предоставляют низкоуровневый доступ к механике кэширования, синхронизации и обновления данных, но при масштабировании приложения они начинают дублироваться в разных компонентах. Именно кастомные хуки становятся ключевым инструментом для формирования устойчивой архитектуры слоя данных.

Кастомный хук в контексте TanStack Query — это абстракция над useQuery, useMutation, useQueryClient и вспомогательными API библиотеки, объединяющая бизнес-логику получения данных, управление ключами запросов и стратегиями обновления кэша.

Основная цель — вынести повторяющиеся запросы и операции с серверным состоянием из компонентов, сохранив единый источник правды для работы с API.


Инкапсуляция query-логики

Базовая проблема при использовании useQuery напрямую заключается в рассеивании логики по компонентам:

  • ключи запросов повторяются
  • функции fetcher дублируются
  • настройки кеширования расходятся
  • обработка ошибок становится несогласованной

Кастомный хук устраняет эти проблемы за счёт централизации.

Пример инкапсуляции:

import { useQuery } from '@tanstack/react-query'

async function fetchUser(userId) {
  const res = await fetch(`/api/users/${userId}`)
  if (!res.ok) throw new Error('Ошибка загрузки пользователя')
  return res.json()
}

export function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: !!userId,
    staleTime: 1000 * 60 * 5
  })
}

Здесь формируется единый контракт:

  • queryKey стандартизирован
  • логика запроса скрыта
  • настройки кеширования централизованы
  • компоненты получают только результат

Структурирование query keys внутри кастомных хуков

Одной из ключевых проблем масштабирования TanStack Query является управление ключами запросов. Ошибки в ключах приводят к некорректному кэшированию и дублированию запросов.

Кастомные хуки позволяют фиксировать структуру ключей внутри одной точки.

const userKeys = {
  all: ['users'],
  detail: (id) => ['users', 'detail', id],
  list: (filters) => ['users', 'list', filters]
}

Использование внутри хука:

export function useUser(userId) {
  return useQuery({
    queryKey: userKeys.detail(userId),
    queryFn: () => fetchUser(userId)
  })
}

Преимущества подхода:

  • предотвращение ошибок в ключах
  • единая система именования
  • упрощение рефакторинга
  • возможность централизованного изменения структуры кеша

Кастомные хуки как слой API-абстракции

В крупных приложениях TanStack Query не должен напрямую зависеть от структуры API. Любые изменения backend-эндпоинтов не должны затрагивать UI-логику.

Кастомные хуки выступают адаптером между API и клиентским состоянием.

function apiGetUser(id) {
  return fetch(`/v1/internal/users/${id}`).then(r => r.json())
}

export function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => apiGetUser(userId)
  })
}

Если API изменится, корректировка произойдёт только в одном месте.


Комбинирование useQuery и useMutation

Кастомные хуки особенно эффективны при объединении чтения и записи данных в одном домене.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

function updateUser(userId, data) {
  return fetch(`/api/users/${userId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  }).then(r => r.json())
}

export function useUser(userId) {
  const queryClient = useQueryClient()

  const query = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
  })

  const mutation = useMutation({
    mutationFn: (data) => updateUser(userId, data),
    onSuccess: (updatedUser) => {
      queryClient.setQueryData(['user', userId], updatedUser)
    }
  })

  return {
    ...query,
    updateUser: mutation.mutate,
    isUpdating: mutation.isPending
  }
}

Такой подход объединяет:

  • получение данных
  • обновление данных
  • синхронизацию кеша

Компонент получает единый интерфейс вместо набора разрозненных хуков.


Управление побочными эффектами внутри кастомных хуков

Кастомные хуки позволяют централизовать не только запросы, но и побочные эффекты:

  • инвалидирование кеша
  • оптимистические обновления
  • откаты при ошибках
  • цепочки зависимых запросов

Пример инвалидирования:

export function useCreatePost() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (data) =>
      fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(data)
      }).then(r => r.json()),

    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['posts']
      })
    }
  })
}

Ключевая идея — бизнес-правила обновления данных живут рядом с операцией изменения данных, а не в компонентах.


Композиция кастомных хуков

TanStack Query естественно поддерживает композицию логики через хуки. Кастомные хуки можно комбинировать между собой для построения сложных сценариев.

Пример зависимого запроса:

export function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
    enabled: !!userId
  })
}

export function useUserPosts(userId) {
  const userQuery = useUser(userId)

  return useQuery({
    queryKey: ['posts', 'user', userId],
    queryFn: () =>
      fetch(`/api/users/${userId}/posts`).then(r => r.json()),
    enabled: !!userQuery.data
  })
}

Такой подход позволяет:

  • связывать данные разных сущностей
  • управлять зависимостями запросов
  • переиспользовать базовые хуки

Разделение уровней: domain hooks и feature hooks

В зрелой архитектуре кастомные хуки делятся на уровни:

Domain hooks

Работают с одной сущностью:

  • useUser
  • usePost
  • useComment

Они инкапсулируют доступ к API и кеш.

Feature hooks

Комбинируют несколько domain hooks:

  • useUserProfilePage
  • usePostEditor
  • useDashboardData

Пример feature-слоя:

export function useUserProfilePage(userId) {
  const user = useUser(userId)
  const posts = useUserPosts(userId)

  return {
    user,
    posts,
    isLoading: user.isLoading || posts.isLoading
  }
}

Это разделение снижает связанность и упрощает тестирование.


Тестирование кастомных хуков TanStack Query

Кастомные хуки упрощают тестирование за счёт изоляции логики.

Основные подходы:

  • мокирование fetcher-функций
  • использование QueryClient в тестовой среде
  • проверка состояния кеша

Пример:

import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function wrapper({ children }) {
  const client = new QueryClient()
  return (
    <QueryClientProvider client={client}>
      {children}
    </QueryClientProvider>
  )
}

test('useUser returns data', async () => {
  const { result } = renderHook(() => useUser(1), { wrapper })

  await waitFor(() => result.current.isSuccess)

  expect(result.current.data).toBeDefined()
})

Кастомные хуки делают тестирование предсказуемым, поскольку скрывают внутреннюю реализацию запросов.


Ошибки проектирования кастомных хуков

При неправильном использовании кастомные хуки могут привести к деградации архитектуры:

  • смешивание UI-логики и data-layer
  • отсутствие унификации query keys
  • дублирование mutation logic
  • создание слишком крупных “god hooks”
  • утечка деталей API в компоненты

Особенно критична ситуация, когда кастомный хук начинает включать состояние интерфейса (например, модальные окна или локальные формы), нарушая разделение ответственности.


Масштабирование через фабрики хуков

В системах с большим количеством однотипных сущностей используется генерация хуков через фабрики.

function createEntityHooks(entityName) {
  return {
    useList: () =>
      useQuery({
        queryKey: [entityName, 'list'],
        queryFn: () => fetch(`/api/${entityName}`).then(r => r.json())
      }),

    useDetail: (id) =>
      useQuery({
        queryKey: [entityName, 'detail', id],
        queryFn: () =>
          fetch(`/api/${entityName}/${id}`).then(r => r.json())
      })
  }
}

export const userHooks = createEntityHooks('users')
export const postHooks = createEntityHooks('posts')

Фабрики позволяют:

  • сократить повторяющийся код
  • унифицировать доступ к API
  • быстро расширять доменную модель

Кастомные хуки как основа архитектуры TanStack Query

При грамотном проектировании кастомные хуки становятся центральным уровнем взаимодействия с серверным состоянием. Они формируют стабильный API для компонентов, скрывают детали реализации запросов, управляют кешем и обеспечивают предсказуемое поведение данных во всём приложении.