Фасетный поиск в eCommerce — штука коварная. Пока фильтров три и категорий пять — можно написать примерно любое своё решение и оно будет работать. Но когда каталог растёт, появляются десятки фасетов, динамические атрибуты, а пользователи начинают кликать по фильтрам быстрее, чем успевает обновляться интерфейс, — тут-то и начинаются сложности.

В этой статье я расскажу, как мы прошли путь от самописного велосипеда через InstantSearch.js с кастомным клиентом до связки TanStack Query + nuqs. С костылями, сомнениями и парой архитектурных «а что, если…».

Важно: это описание нашего практического опыта, а не истина в последней инстанции. Возможно, мы что-то делали не так, где-то не докрутили, а где-то луна была не в той фазе. Если у вас получилось подружить InstantSearch с кастомным поисковым движком без лишних проблем — мы только за. Нам же хочется поделиться тем, к чему пришли сами, и, возможно, сэкономить кому-то время.


Контекст

B2C eCommerce-платформа с большим катологом продуктов. Гибридный SSR/CSR, SEO важен, пользователи активно используют фильтры.

Составляющие

  • Каталог — 40+ миллионов SKU, вариативные товары.

  • Поисковый движок — Manticore Search.

  • Фронтенд — Next.js, UI-компоненты на базе shadcn/ui.

  • UI каталога — фильтры (фасеты), сортировка, пагинация, синхронизация состояния с URL.

Особенности

  • 15–20+ фасетов на категорию, плюс динамические атрибуты, которые PIM-система может менять без участия разработчиков.

  • Disjunctive faceting — логика «ИЛИ» внутри фасета (например, несколько брендов) и «И» между разными фасетами.

  • SSR обязателен — гидратация без мерцаний и двойных запросов.

  • Быстрый отклик и предсказуемая нагрузка на поисковый движок.


Этап 1. Самописная реализация «на коленке»

На старте было просто: пара фильтров, прямые запросы в Manticore, состояние в useContext, URL‑синхронизация руками.

Что пошло не так:

  • Каждый новый фильтр требовал правки в нескольких местах: запросы, агрегации, UI.

  • Disjunctive faceting превращал код в «лапшу» из ветвлений.

  • Постоянное дублирование логики между SSR и CSR, плюс мерцания при гидратации.

Прорыв (который нас и подтолкнул к InstantSearch):
Мы поняли, что фильтры должны быть динамическими — их набор и возможные значения определяются PIM и могут меняться без переписывания фронтенда. Ручное добавление каждого фасета перестало масштабироваться. Статические фильтры тоже остаются (цена, наличие), но всё остальное можно передать в динамику.


Этап 2. InstantSearch.js с кастомным клиентом (без Algolia)

Звучит логично: есть библиотека, которая умеет всё — фасеты, состояние, синхронизацию с URL. Берём, адаптируем под Manticore, получаем профит.

Спойлер: есть нюансы.

С чем мы столкнулись

  • Двойной запрос при SSR. Server action для SSR + API handler для клиента. Из-за клиентской природы InstantSearch вызов API не мог идти через server action.

  • Каждый смонтированный фильтр = отдельный запрос. Кеш спасал, но рассинхрон был возможен.

  • Документации по кастомному searchClient нет. Формат данных искали методом тыка.

  • InstantSearch превратился из помощника в препятствие.

    • Появились вынужденные прослойки вроде FiltersController.

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

    • Отдельный квест — первый клиентский переход. Если сначала открыть главную страницу, а затем перейти на страницу с фильтрами — поведение отличалось от прямой загрузки (SSR).

  • Гонка состояний. Пользователь кликает быстро — результат непредсказуем. Пришлось блокировать интерфейс на время загрузки.

Сомнения и вывод

В процессе реализации то и дело возникал вопрос: «А может, мы выбрали не тот путь?» Ведь библиотека написана умными людьми, потрачено много усилий — скорее всего, это мы что-то делаем не так.

Сейчас мы считаем, что InstantSearch — отличное решение, когда вы работаете в экосистеме Algolia. Там он даёт простоту, хороший SSR и предсказуемое поведение. Но как только возникает необходимость в кастомном searchClient (свой поисковый движок, нестандартная бизнес-логика), выгода от использования InstantSearch стремительно тает, а цена поддержки растёт.

Сроки: около трёх месяцев до стабильной версии (с костылями, но работающей).


Этап 3. TanStack Query как слой серверного состояния

Когда стало понятно, что InstantSearch не наш путь, мы решили: оставляем API как есть, но выкидываем InstantSearch. Всё, что связано с кэшированием, дедупликацией и передачей состояния с сервера на клиент, отдаём TanStack Query. URL‑синхронизацию — отдельному хуку на nuqs.

Ключевые отличия от InstantSearch:

  • Нет API handler — всё через server action.

  • Нет FiltersController и стейт-менеджеров — состояние фильтров живёт в URL и queryKey.

  • При изменении фильтра — один запрос, без дублей.

Любое изменение фильтров меняет queryKey, и TanStack Query сам решает, отдать данные из кэша или сходить за новыми.

Что мы получили

  • Контроль и предсказуемость. Запросы, агрегации, инвалидация — всё явно, нет чёрных ящиков.

  • Меньше сетевых вызовов. Кэш + дедупликация.

  • Стабильный SSR. Один запрос на сервере, гидратация без повторного fetch.

  • Исчезла блокировка интерфейса. Можно кликать с любой скоростью — пользователь всегда увидит результат последнего изменения.

  • Проще развивать. Добавление нового статического фильтра — расширить queryKey и UI-компонент. Без адаптеров и костылей.

  • Оптимистичные обновления. Гулять так гулять. Пользователь видит изменение мгновенно, а запрос идёт в фоне. При быстрых кликах интерфейс не подвисает.

Сроки: около двух недель на переход. Почти все наработки по API и динамическим фильтрам переиспользовали.

Минусы TanStack Query

  • Порог входа выше, чем у InstantSearch + Algolia. Но он понятный и предсказуемый. Всё ок с документацией.

  • Больше ручной работы. Нет готовых UI компонентов. В нашем случае UI уже был кастомным, так что не заметили.

  • Риск раздуть кэш при неаккуратных ключах.

  • SSR-гидратация всё равно требует внимания к архитектуре. TanStack Query решает проблемы данных, но не роутинг и не синхронизацию с URL.

Резюме

В итоге мы получили предсказуемое, быстрое и понятное решение. Основные изменения:

  • Кода стало меньше — исчез слой адаптера под Algolia-контракты и FiltersController.

  • Состояние фильтров живёт в URL и нормализовано в queryKey.

  • SSR работает без костылей — один запрос на сервере, гидратация без повторного fetch.

  • Дедупликация и кэш снизили нагрузку на движок.

Что выбрать?

  • InstantSearch.js — отличное решение, если вы работаете в экосистеме Algolia. Быстро, просто, с хорошим (возможно) SSR из коробки.

  • TanStack Query + nuqs — когда у вас кастомный поисковый движок, нестандартная логика фасетов или вы не хотите привязываться к Algolia. Даёт контроль и прозрачность, но требует больше ручной работы.

Наш опыт показывает: для кастомного бэкенда связка TanStack Query + nuqs оказалась проще, быстрее и дешевле в поддержке, чем попытки подружить InstantSearch с Manticore.

Комментарии (0)