TanStack Query: Майстерність Керування Станом Сервера

Аутентифікація та Обробка Помилок

Робота з захищеними даними (Authorized only) додає шар складності. Що робити, якщо токен протух? Як глобально показати помилку?

Аутентифікація та Обробка Помилок

Робота з захищеними даними (Authorized only) додає шар складності. Що робити, якщо токен протух? Як глобально показати помилку?

Глобальна Обробка Помилок

Замість того, щоб писати if (isError) у кожному компоненті, ми можемо перехоплювати помилки на рівні QueryCache.

queryClient.ts
import { QueryCache, MutationCache, QueryClient } from '@tanstack/react-query';
import { toast } from 'react-hot-toast';

export const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // Ігноруємо помилки, якщо ми їх обробляємо локально (наприклад, у формі)
      if (query.meta?.errorMessage) {
        toast.error(query.meta.errorMessage);
      } else {
        toast.error(`Global Error: ${error.message}`);
      }
    },
  }),
  mutationCache: new MutationCache({
    onError: (error) => {
      toast.error(error.message);
    },
  }),
});

Використання meta:

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  meta: {
    errorMessage: 'Не вдалося завантажити список справ',
  },
});

JWT та Refresh Token

Це класична проблема. У вас є короткоживучий accessToken і довгоживучий refreshToken. Коли accessToken вмирає, сервер повертає 401.

Вам потрібно:

  1. Перехопити 401.
  2. Поставити всі інші запити на паузу.
  3. Зробити запит на /refresh.
  4. Оновити токен.
  5. Повторити оригінальний запит.

Це НЕ завдання TanStack Query. Це завдання вашого HTTP-клієнта (Axios).

Axios Interceptor Pattern

TanStack Query нічого не знає про токени. Він просто викликає функцію. Якщо функція кидає помилку — він переходить в status: error.

Але якщо Axios "тихо" оновить токен і повторить запит, TanStack Query навіть не помітить, що була помилка. Це ідеальний сценарій.

api/axios.ts
import axios from 'axios';

const api = axios.create({ baseURL: '/api' });

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // Якщо 401 і це не повторна спроба
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const { data } = await axios.post('/api/refresh-token');
        // Оновлюємо токен в хедерах
        api.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
        originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
        
        // Повторюємо оригінальний запит
        return api(originalRequest);
      } catch (refreshError) {
        // Якщо refresh теж впав — юзер має залогінитись заново
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

Відключення запитів без токена

Якщо у вас немає токена (наприклад, юзер не залогінений), не треба навіть намагатися робити запити.

const { accessToken } = useAuth();

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  // Запит полетить тільки якщо є токен
  enabled: !!accessToken, 
});

Logout: Очищення Кешу

Коли користувач виходить з системи, критично важливо очистити кеш. Інакше наступний користувач може побачити дані попереднього (якщо вони сядуть за один комп'ютер).

const logout = () => {
  // 1. Видаляємо токен
  localStorage.removeItem('token');
  
  // 2. Очищаємо весь кеш
  queryClient.clear();
  
  // Або видаляємо конкретні запити
  // queryClient.removeQueries(); 
};
queryClient.clear() видаляє всі дані, але не зупиняє активні запити. queryClient.resetQueries() скидає дані до початкового стану (і може спровокувати refetch). Для логауту найкраще clear() або removeQueries().
Copyright © 2026