Поиск с автодополнением

Поиск с автодополнением строится вокруг непрерывного потока запросов, которые пользователь генерирует при вводе текста. В классической реализации без специализированных инструментов это приводит к проблемам: избыточные сетевые запросы, гонки ответов, устаревшие результаты и сложное управление состоянием загрузки. TanStack Query позволяет структурировать этот процесс через декларативные запросы, кэширование и управление состоянием сервера.

Автодополнение обычно строится вокруг одного динамического параметра — строки запроса. Каждое изменение ввода пользователя инициирует новый запрос к API:

const fetchSuggestions = async (query) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  if (!res.ok) throw new Error("Network error");
  return res.json();
};

В классическом подходе это быстро приводит к лавине запросов. Основная задача — связать ввод пользователя и серверное состояние так, чтобы:

  • не отправлять запрос на каждое нажатие клавиши без контроля
  • использовать кэш для повторяющихся запросов
  • избегать устаревших ответов
  • корректно обрабатывать состояния loading/error/empty

Интеграция с TanStack Query

В TanStack Query автодополнение реализуется через динамический queryKey, зависящий от строки поиска.

import { useQuery } from "@tanstack/react-query";

const useSearchSuggestions = (query) => {
  return useQuery({
    queryKey: ["autocomplete", query],
    queryFn: () => fetchSuggestions(query),
    enabled: query.length > 0
  });
};

Ключевая идея заключается в том, что каждый уникальный запрос автоматически кэшируется. При повторном вводе уже встречавшегося значения данные берутся из памяти без сетевого запроса.

Управление частотой запросов

Проблема частых обновлений решается не внутри TanStack Query, а на уровне UI-логики через debounce. Это предотвращает создание новых query слишком часто.

import { useState, useEffect } from "react";

function useDebouncedValue(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

Использование:

const debouncedQuery = useDebouncedValue(query, 300);

const { data, isLoading } = useQuery({
  queryKey: ["autocomplete", debouncedQuery],
  queryFn: () => fetchSuggestions(debouncedQuery),
  enabled: debouncedQuery.length > 0
});

Такая комбинация снижает нагрузку на сервер и предотвращает избыточные перерендеры.

Кэширование и повторное использование результатов

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

Поведение кэша зависит от нескольких параметров:

  • staleTime — время, в течение которого данные считаются актуальными
  • cacheTime — время хранения данных в памяти
  • gcTime (в новых версиях) — управление сборкой мусора

Пример настройки:

useQuery({
  queryKey: ["autocomplete", debouncedQuery],
  queryFn: () => fetchSuggestions(debouncedQuery),
  staleTime: 1000 * 60, // 1 минута
  gcTime: 1000 * 60 * 10
});

При высокой частоте повторений это значительно снижает количество сетевых обращений.

Отмена устаревших запросов

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

const fetchSuggestions = async ({ queryKey, signal }) => {
  const [, query] = queryKey;

  const res = await fetch(`/api/search?q=${query}`, {
    signal
  });

  if (!res.ok) throw new Error("Network error");
  return res.json();
};

TanStack Query автоматически передаёт signal, позволяя браузеру отменять предыдущие запросы при изменении queryKey.

Состояния UI в автодополнении

Корректное отображение состояния критично для UX:

  • пустой ввод
  • загрузка
  • результаты
  • отсутствие результатов
  • ошибка

Пример обработки:

const { data, isLoading, isError } = useQuery({
  queryKey: ["autocomplete", debouncedQuery],
  queryFn: () => fetchSuggestions(debouncedQuery),
  enabled: debouncedQuery.length > 0
});

if (debouncedQuery.length === 0) {
  return null;
}

if (isLoading) {
  return <div>Загрузка...</div>;
}

if (isError) {
  return <div>Ошибка загрузки</div>;
}

if (!data?.length) {
  return <div>Ничего не найдено</div>;
}

return (
  <ul>
    {data.map(item => (
      <li key={item.id}>{item.title}</li>
    ))}
  </ul>
);

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

Автодополнение может быть дополнено стратегией предзагрузки. Это уменьшает задержку первого запроса.

import { useQueryClient } from "@tanstack/react-query";

const queryClient = useQueryClient();

const prefetchSuggestions = (query) => {
  queryClient.prefetchQuery({
    queryKey: ["autocomplete", query],
    queryFn: () => fetchSuggestions(query),
    staleTime: 1000 * 60
  });
};

Предзагрузка часто используется для популярных или трендовых запросов, а также при наведении курсора или фокусе на поле ввода.

Управление зависимыми запросами

Иногда автодополнение зависит от дополнительных параметров: языка, региона или типа поиска.

const useSearch = (query, locale) => {
  return useQuery({
    queryKey: ["autocomplete", locale, query],
    queryFn: () => fetch(`/api/search?q=${query}&locale=${locale}`).then(r => r.json()),
    enabled: query.length > 0
  });
};

Расширение queryKey обеспечивает изоляцию кэша между различными контекстами.

Интеграция с серверной фильтрацией

Автодополнение часто является частью серверной фильтрации. В таких случаях важно не смешивать локальную фильтрацию и серверные данные.

const useFilteredSuggestions = (query, type) => {
  return useQuery({
    queryKey: ["autocomplete", type, query],
    queryFn: () => fetch(`/api/search?type=${type}&q=${query}`).then(r => r.json()),
    enabled: query.length > 2
  });
};

Ограничение минимальной длины запроса снижает нагрузку на API и улучшает релевантность.

Контроль гонок через ключи запроса

Структура queryKey является центральным механизмом синхронизации состояния. Любое изменение ключа:

  • запускает новый запрос
  • инвалидирует предыдущий
  • может отменить активный запрос

Типичная ошибка — использование нестабильных объектов:

// плохо
queryKey: ["autocomplete", { query }]

// лучше
queryKey: ["autocomplete", query]

Стабильность ключей напрямую влияет на предсказуемость кэша.

UX-оптимизация через keepPreviousData

При автодополнении важно избегать «мигания» интерфейса при смене запроса. Опция placeholderData или keepPreviousData позволяет сохранять предыдущие результаты до прихода новых:

import { keepPreviousData } from "@tanstack/react-query";

useQuery({
  queryKey: ["autocomplete", debouncedQuery],
  queryFn: () => fetchSuggestions(debouncedQuery),
  placeholderData: keepPreviousData
});

Это создаёт ощущение непрерывности интерфейса.

Расширенные сценарии

Автодополнение может включать сложные сценарии:

  • комбинированный поиск (по названию и категории)
  • ранжирование результатов на клиенте
  • объединение нескольких источников данных
  • параллельные запросы

Пример параллельных источников:

const useMultiSourceSearch = (query) => {
  const local = useQuery({
    queryKey: ["local", query],
    queryFn: () => fetch(`/api/local?q=${query}`).then(r => r.json()),
    enabled: query.length > 1
  });

  const remote = useQuery({
    queryKey: ["remote", query],
    queryFn: () => fetch(`/api/remote?q=${query}`).then(r => r.json()),
    enabled: query.length > 1
  });

  return {
    data: [...(local.data || []), ...(remote.data || [])],
    isLoading: local.isLoading || remote.isLoading
  };
};

Такая схема позволяет объединять источники без ручного управления состоянием загрузки каждого запроса.

Обработка ошибок и деградация

В системах автодополнения важно не блокировать интерфейс при сбое API. TanStack Query позволяет реализовать мягкую деградацию через retry и retryDelay:

useQuery({
  queryKey: ["autocomplete", query],
  queryFn: () => fetchSuggestions(query),
  retry: 1,
  retryDelay: 500
});

При этом UI может продолжать отображать предыдущие результаты из кэша, снижая влияние ошибок сети на пользовательский опыт.