TanStack Query в JavaScript строится вокруг идеи инкапсуляции
серверного состояния и его жизненного цикла. Базовые хуки вроде
useQuery и useMutation предоставляют
низкоуровневый доступ к механике кэширования, синхронизации и обновления
данных, но при масштабировании приложения они начинают дублироваться в
разных компонентах. Именно кастомные хуки становятся ключевым
инструментом для формирования устойчивой архитектуры слоя данных.
Кастомный хук в контексте TanStack Query — это абстракция над
useQuery, useMutation,
useQueryClient и вспомогательными API библиотеки,
объединяющая бизнес-логику получения данных, управление ключами запросов
и стратегиями обновления кэша.
Основная цель — вынести повторяющиеся запросы и операции с серверным состоянием из компонентов, сохранив единый источник правды для работы с API.
Базовая проблема при использовании useQuery напрямую
заключается в рассеивании логики по компонентам:
Кастомный хук устраняет эти проблемы за счёт централизации.
Пример инкапсуляции:
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 стандартизированОдной из ключевых проблем масштабирования 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)
})
}
Преимущества подхода:
В крупных приложениях 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 изменится, корректировка произойдёт только в одном месте.
Кастомные хуки особенно эффективны при объединении чтения и записи данных в одном домене.
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
})
}
Такой подход позволяет:
В зрелой архитектуре кастомные хуки делятся на уровни:
Работают с одной сущностью:
useUserusePostuseCommentОни инкапсулируют доступ к API и кеш.
Комбинируют несколько domain hooks:
useUserProfilePageusePostEditoruseDashboardDataПример feature-слоя:
export function useUserProfilePage(userId) {
const user = useUser(userId)
const posts = useUserPosts(userId)
return {
user,
posts,
isLoading: user.isLoading || posts.isLoading
}
}
Это разделение снижает связанность и упрощает тестирование.
Кастомные хуки упрощают тестирование за счёт изоляции логики.
Основные подходы:
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()
})
Кастомные хуки делают тестирование предсказуемым, поскольку скрывают внутреннюю реализацию запросов.
При неправильном использовании кастомные хуки могут привести к деградации архитектуры:
Особенно критична ситуация, когда кастомный хук начинает включать состояние интерфейса (например, модальные окна или локальные формы), нарушая разделение ответственности.
В системах с большим количеством однотипных сущностей используется генерация хуков через фабрики.
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 для компонентов, скрывают детали реализации запросов, управляют кешем и обеспечивают предсказуемое поведение данных во всём приложении.