Налаштування 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, написання користувацьких хуків для всіх кінцевих точок, мутації з оптимістичним оновленням, інвалідація, 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>
  )
}

Ключі запитів — конвенція

Ключ запиту — унікальний ідентифікатор запиту. Кеш та інвалідація залежать від нього:

// 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, // не миготить при переході сторінок
  })
}

Нескінченна прокрутка

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 при наведенні на посилання
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 з розумними параметрами за замовчуванням, проектуємо ієрархію ключів запитів, пишемо користувацькі хуки для всіх API-кінцевих точок, реалізуємо мутації з оптимістичним оновленням для критичних форм, налаштовуємо prefetching та інвалідацію, при необхідності інтегруємо з SSR.

Строк: 2–5 днів залежно від кількості кінцевих точок та наявності SSR.