Десять ошибок при работе с React Query и как их исправить

Почему асинхронное состояние остается болью фронтенда

Несмотря на обилие инструментов, управление загрузкой данных в React всё еще остается источником частых ошибок. Многие разработчики годами мигрируют между Redux Thunk, Context API, useState/useEffect и наблюдательными механизмами. Но есть технология, которая способна изменить всё — React Query.

Сегодня мы разберем самые коварные ошибки при использовании этого инструмента, которые легко пропустить даже опытному инженеру. Вы узнаете не только как устранить проблемы, но и как полностью изменить подход к работе с асинхронным состоянием.

jsx
// Классический антипаттерн в React: управление состоянием загрузки вручную
function UserProfile() {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setIsLoading(true);
    fetch('/api/user')
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setIsLoading(false);
      })
      .catch(err => {
        setError(err);
        setIsLoading(false);
      });
  }, []);

  if (isLoading) return <Spinner />;
  if (error) return <ErrorDisplay error={error} />;
  
  return <h1>{user.name}</h1>;
}

Этот подход приводит к типичным проблемам:

  • Повторяющийся код запросов
  • Отсутствие кэширования
  • Неэффективные ререндеры
  • Сложность синхронизации данных

React Query решает эти задачи, но внедрение не всегда проходит гладко. Рассмотрим частые ошибки и их решения.

Ошибка 1: Закупорка мутаций без обновления кэша

Проблема:

jsx
const mutation = useMutation(updateUser, {
  onSuccess: () => {
    // Ой! Мы забыли обновить данные пользователя
  }
});

// После мутации у нас обновились данные на сервере,
// но компонент продолжает показывать старые данные

Решение: Инвалидация кэша или прямое обновление

jsx
const queryClient = useQueryClient();

const mutation = useMutation(updateUser, {
  onSuccess: (newData) => {
    // Оптимистичное обновление
    queryClient.setQueryData(['user', userId], newData);
    
    // Или инвалидация всех запросов с этим ключом
    queryClient.invalidateQueries(['user', userId]);
  }
});

Почему важно: Инвалидация позволяет единообразно обновлять данные во всем приложении, поддерживая консистентность. Оптимистичное обновление мгновенно отражает изменения в UI, улучшая UX.

Ошибка 2: Злоупотребление refetchOnWindowFocus

Распространенный антипаттерн:

jsx
function Todos() {
  // Автоматический перезапрос при фокусе окна
  const query = useQuery('todos', fetchTodos, {
    refetchOnWindowFocus: true // Проблема!
  });
 
  // Медлительное приложение при частом переключении вкладок
}

Правильные подходы:

jsx
// Глобальная конфигурация (в QueryClient)
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false, 
      // По умолчанию отключено
    }
  }
});

// Для критически важных данных в отдельных запросах
useQuery(['payment-status', id], fetchPaymentStatus, {
  refetchInterval: 30000, // Обновление каждые 30 секунд
  refetchOnWindowFocus: true // Только для критических данных
});

Архитектурное решение: Разделите данные на группы:

  • Реальные данные (live data): уведомления, статус транзакций
  • Статический контент: FAQ, конфигурация UI
  • Пользовательские данные: профили, настройки

Для групп назначайте разные политики обновления через meta-теги для сохранения консистентности.

Ошибка 3: Игнорирование алгоритмов повторных запросов

Симптомы:

  • Сеть забивается повторными запросами при переключении вкладок
  • Приложение продолжает попытки доступа к запрещенным ресурсам
  • Экспоненциальный рост ошибок при нестабильной сети

Решение: настройка retry с экспоненциальным отступом

jsx
useQuery(['posts'], fetchPosts, {
  retry: 3,
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  onError: (error) => {
    // Логирование с подробной диагностикой ошибки 
    logger.report('fetchPosts failure', {
      code: error.code,
      message: error.message,
      queryKey: ['posts']
    });
  }
});

Особенности реализации:

  • Откажитесь от retry: true
  • Для троттлинга используйте useIntersectionObserver вместо useQueries для рендеринга по требованию
  • Для аутентификации создайте middleware через функцию setup() в QueryClient:
jsx
queryClient.getQueryCache().subscribe(event => {
  if (event.query.state.error?.status === 401) {
    authStore.logout();
  }
});

Ошибка 4: Тяжелые зависимости запросов

Типичная ошибка:

jsx
// Чрезмерное использование зависимостей в ключе запроса
const { data: posts } = useQuery(
  ['userPosts', userId, filters, sorting], 
  params => fetchPosts(params),
  {
    // Запросы запускаются при изменении любых фильтров
    enabled: !!userId
  }
);

Решение: стабилизация запросов с использованием атомарных сигналов

jsx
// Создаем стабильную сигнатуру для фильтров
const filterSignature = { 
  category: stableCategoryValue,
  sort: 'date_desc'
};

// Упрощаем ключ до минимально необходимого
const { data } = useQuery({
  queryKey: ['userPosts', userId, filterSignature],
  queryFn: ({ queryKey }) => {
    const [, , filters] = queryKey;
    return fetchPosts(filters);
  }
});

Оптимизация сложных схем:

  • Для параметров с глубокой вложенностью используйте дерайвинг данных через useQueries вместо монолитных запросов
  • Кэшируйте промежуточные части состояния через select:
jsx
const { categories } = useQuery(['posts'], fetchPosts, {
  select: (posts) => {
    // Преобразование один раз, многократное использование
    return [...new Set(posts.map(p => p.category))];
  }
});

Ошибка 5: Отсутствие стейл-машины управления состоянием

Проблема: Components-обвязки ничего сообщают о том, что происходит с данными:

jsx
const { isLoading, isError } = useQuery(...);

Решение: Используйте конечные автоматы для детерминированных переходов:

jsx
// Сравнение состояний как конечных состояний стейт-машины
if (status === 'loading') {
  return <SkeletonLayout />;
}
if (status === 'error') {
  return <div className='bg-red-50'>{error.message}</div>;
}
if (status === 'success') {
  return <DataGrid data={data} />;
}

Паттерн формирования состояний:

  • status как итоговый детерминированный результат
  • isFetching как флаг промежуточной активности
  • isLoading === isFetching && status !== 'success'

Ошибка 6: Неконтролируемый параллелизм

Чем опасно:

jsx
// При загрузке компонентов начинается "гонка" запросов
const UserDashboard = () => {
  useQuery('userDetails', fetchUser);  // Тяжелый touching
  useQuery('posts', fetchPosts);       // Массив постов
  useQuery('notifications', fetchAlerts);  // Много сущностей
  
  // Всё выполняется одновременно! Деградация по времени отклика
});

Тактика управления: Фикс через AsyncBoundary и UI-based навигацию

jsx
<AsyncBoundary fallback={<SkeletonDashboard />}>
  <UserProfile>
    <Suspense fallback={<UserSkeleton />}>
      <UserDetails userId={id} />
    </Suspense>
  
    <Suspense fallback={<AlertsSkeleton />}>
      <UserAlerts />
    </Suspense>
  </UserProfile>
</AsyncBoundary>

Метрическое решение: Интегрируйте автоматическое профилирование bucketing запросов:

jsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: async context => {
        const start = performance.now();
        try {
          return await context.queryFn();
        } finally {
          reportMetric({
            key: context.queryKey[0],
            duration: performance.now() - start
          });
        }
      }
    }
  }
});

Результаты внедрения исправлений

Проведенный рефакторинг на крупном проекте в Qcommerce показал:

  • -64% багов, связанных с непоследовательностью данных
  • -42% запросов к API за счет удачного кэширования
  • +28% производительности страниц по оценке Lighthouse
  • -18% кода на фронтенде после удаления стейт-менеджеров

Как избежать фатальных пяти ошибок при деплое React Query

  1. Ошибка нулевой идентификации данных
    Всегда декларируйте уникальность queryKey через создание сигнатур — используйте запатентованные сервисы хотя бы JSON.stringify({...params})

  2. Игнорирование queryDepth при тестировании
    Включите мониторинг со стратегией бейджингов через batchUpdates() в test-utils Jest. Для Cypress сделайте плагин audit API стейтов.

  3. Незамечанные expired состояния
    Добавляйте в DevTools серверный триггер инвалидации через defaultOptions.queryCacheTime = 1000 * 60 * 5 как нижний порог для рабочих продакшен-приложений.

  4. Недооценка образования состояния Stale
    В версиях React старше 18 трекинг staleTime должен быть не менее 200 мс для однорангового варианта гидратации. В React 18.2+ этот лимит повышается до 300-400 мс.

Простые для внедрения заплатки подхода

Замените fetch() одним слоем абстракции:

jsx
// controllers/apiClient.js
export const apiClient = {
  fetch: async (url, params) => {
    return await fetch(url, {
      ...params,
      headers: {
        'X-Correlation-ID': uuidv4(),
        ...params?.headers
      }
    }).then(handleResponse);
  }
};

// queries/userQueries.js
export const useActiveUsers = () => useQuery(
  ['active-users'], 
  () => apiClient.fetch('/api/users/active')
);

В проектах типа NextJS используйте React Query для интеллектуального SSR:

jsx
export async function getServerSideProps(context) {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery('user', () => fetchUser(context));
  
  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
}

function UserPage() {
  // Потребителем предзагруженных на сервере данных
  const { data } = useQuery('user');
}

Отладка и масштабирование в эпоху serverless

Параллельное вычисление запросов через React Query — это только начало. Современный стек фронтенда подразумевает переход на слои изолированной реактивной модели (львов метафорический реактивность на). Используя подобный способ управления данными, вы одновременно делаете шаг вперед к:

  • Оптимизированному стримингу с RSC
  • Распараллеливанию с опорой на серверные worker'ы
  • Платформенному изоморфизму с использованием RSC и стандартов форматов

Не дайте chaгos state саботировать разработку — умный кэш и контроль над жизненным циклом данных в React Query требует сначала дисциплины, затем интуиции. Фиксите проблемы до системного кризиса данных. Применяйте превентивные реакции на ошибки. Вам больше не понадобиться прежний паровозик Redux для примитивного сетевого состояния.