Настройка React Query (TanStack Query) для управления серверным состоянием

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

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка React Query (TanStack Query) для управления серверным состоянием
Средняя
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Настройка React Query (TanStack Query) для управления серверным состоянием

React Query разделяет два принципиально разных вида состояния: клиентское (UI, формы) и серверное (данные с API). Серверное состояние — асинхронное, кэшируемое, устаревающее. React Query берёт на себя кэш, фоновые обновления, дедупликацию запросов, пагинацию и инвалидацию.

Результат: уходит ~60–70% кода для работы с данными — loading, error, useEffect + fetch заменяются одним хуком.

Что входит в работу

Установка и настройка QueryClient, написание кастомных хуков для всех endpoints, мутации с оптимистичным обновлением, инвалидация, prefetching, пагинация/infinite scroll, интеграция с серверным рендерингом (SSR/Next.js), DevTools.

Установка

npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,      // данные свежие 5 минут
      gcTime: 10 * 60 * 1000,        // кэш хранится 10 минут (бывший cacheTime)
      retry: 2,
      refetchOnWindowFocus: true,
    },
    mutations: {
      retry: 0,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

Query keys — соглашение

Query key — уникальный идентификатор запроса. От него зависит кэш и инвалидация:

// queryKeys.ts
export const queryKeys = {
  products: {
    all: ['products'] as const,
    lists: () => [...queryKeys.products.all, 'list'] as const,
    list: (filters: ProductFilters) => [...queryKeys.products.lists(), filters] as const,
    details: () => [...queryKeys.products.all, 'detail'] as const,
    detail: (id: string) => [...queryKeys.products.details(), id] as const,
  },
  users: {
    all: ['users'] as const,
    me: () => [...queryKeys.users.all, 'me'] as const,
    profile: (id: string) => [...queryKeys.users.all, 'profile', id] as const,
  },
}

Базовые хуки

// hooks/useProducts.ts
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { queryKeys } from '@/lib/queryKeys'

export function useProducts(filters: ProductFilters) {
  return useQuery({
    queryKey: queryKeys.products.list(filters),
    queryFn: () => api.get<Product[]>('/products', { params: filters }),
    placeholderData: (prev) => prev, // предыдущие данные при смене фильтров
  })
}

export function useProduct(id: string) {
  return useQuery({
    queryKey: queryKeys.products.detail(id),
    queryFn: () => api.get<Product>(`/products/${id}`),
    enabled: !!id, // не запрашивать, если id пустой
  })
}

// useSuspenseQuery — бросает промис (для React Suspense)
export function useProductSuspense(id: string) {
  return useSuspenseQuery({
    queryKey: queryKeys.products.detail(id),
    queryFn: () => api.get<Product>(`/products/${id}`),
  })
}

Мутации

// hooks/useProductMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'

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

  return useMutation({
    mutationFn: (data: CreateProductDto) => api.post<Product>('/products', data),

    onSuccess: (newProduct) => {
      // инвалидируем списки
      queryClient.invalidateQueries({ queryKey: queryKeys.products.lists() })
      // сразу кладём в кэш детали
      queryClient.setQueryData(queryKeys.products.detail(newProduct.id), newProduct)
    },

    onError: (error) => {
      toast.error(`Ошибка создания: ${error.message}`)
    },
  })
}

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

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateProductDto }) =>
      api.patch<Product>(`/products/${id}`, data),

    // оптимистичное обновление
    onMutate: async ({ id, data }) => {
      await queryClient.cancelQueries({ queryKey: queryKeys.products.detail(id) })
      const previous = queryClient.getQueryData<Product>(queryKeys.products.detail(id))

      queryClient.setQueryData(queryKeys.products.detail(id), (old: Product) => ({
        ...old,
        ...data,
      }))

      return { previous }
    },

    onError: (_, { id }, context) => {
      // откатываем при ошибке
      if (context?.previous) {
        queryClient.setQueryData(queryKeys.products.detail(id), context.previous)
      }
    },

    onSettled: (_, __, { id }) => {
      queryClient.invalidateQueries({ queryKey: queryKeys.products.detail(id) })
    },
  })
}

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

  return useMutation({
    mutationFn: (id: string) => api.delete(`/products/${id}`),
    onSuccess: (_, id) => {
      queryClient.removeQueries({ queryKey: queryKeys.products.detail(id) })
      queryClient.invalidateQueries({ queryKey: queryKeys.products.lists() })
    },
  })
}

Использование в компоненте

function ProductList({ filters }: { filters: ProductFilters }) {
  const { data, isLoading, isError, error, isFetching } = useProducts(filters)
  const { mutate: createProduct, isPending } = useCreateProduct()

  if (isLoading) return <Skeleton count={6} />
  if (isError) return <ErrorMessage message={error.message} />

  return (
    <div>
      {isFetching && <div className="loading-bar" />}
      <ProductGrid products={data} />
      <button
        onClick={() => createProduct({ name: 'Новый товар', price: 0 })}
        disabled={isPending}
      >
        Добавить
      </button>
    </div>
  )
}

Пагинация

export function useProductsPage(page: number, pageSize = 20) {
  return useQuery({
    queryKey: queryKeys.products.list({ page, pageSize }),
    queryFn: () => api.get<PaginatedResponse<Product>>('/products', { params: { page, pageSize } }),
    placeholderData: keepPreviousData, // не мигает при переходе страниц
  })
}

Infinite scroll

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

export function useInfiniteProducts(filters: Omit<ProductFilters, 'page'>) {
  return useInfiniteQuery({
    queryKey: queryKeys.products.list(filters),
    queryFn: ({ pageParam }) =>
      api.get<PaginatedResponse<Product>>('/products', {
        params: { ...filters, page: pageParam, pageSize: 20 },
      }),
    initialPageParam: 1,
    getNextPageParam: (lastPage) =>
      lastPage.hasNextPage ? lastPage.page + 1 : undefined,
  })
}
function InfiniteProductList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteProducts({})
  const products = data?.pages.flatMap((p) => p.items) ?? []

  return (
    <>
      <ProductGrid products={products} />
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Загрузка...' : 'Ещё'}
        </button>
      )}
    </>
  )
}

Prefetching

// prefetch при hover на ссылку
function ProductLink({ id }: { id: string }) {
  const queryClient = useQueryClient()

  return (
    <Link
      to={`/products/${id}`}
      onMouseEnter={() => {
        queryClient.prefetchQuery({
          queryKey: queryKeys.products.detail(id),
          queryFn: () => api.get<Product>(`/products/${id}`),
          staleTime: 60_000,
        })
      }}
    >
      Перейти
    </Link>
  )
}

SSR с Next.js App Router

// app/products/page.tsx (Next.js 14+)
import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query'
import { ProductList } from './ProductList'

export default async function ProductsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: queryKeys.products.lists(),
    queryFn: () => fetchProductsServer(),
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ProductList />
    </HydrationBoundary>
  )
}

Что делаем

Устанавливаем QueryClient с разумными defaults, проектируем иерархию query keys, пишем кастомные хуки для всех API-endpoint, реализуем мутации с оптимистичным обновлением для критичных форм, настраиваем prefetching и инвалидацию, при необходимости интегрируем с SSR.

Срок: 2–5 дней в зависимости от количества endpoints и наличия SSR.