ENGINEERING NOTES / Complex UI
Сложные фильтры без UX-коллапса
Как связать форму, URL и запросы к API, сохранить предсказуемое поведение поиска и не дать устаревшим ответам испортить результат.
Статья на русском языкеФильтр — это сценарий, а не набор полей
В каталоге фильтр часто начинается с простого поля поиска. Затем появляются диапазоны, несколько категорий, сортировка, карта и пагинация. Пользователь быстро меняет условия, открывает карточку, возвращается назад и ожидает увидеть прежний результат.
Если каждый элемент хранит собственное независимое состояние, форма, адресная строка и выдача постепенно расходятся. На экране выбран один город, в URL остался другой, а данные пришли от предыдущего запроса. Исправлять это удобнее через явную модель сценария, а не отдельные обработчики для каждого поля.
Разделить ввод и применённые параметры
Полезно различать черновик формы и параметры, по которым уже построена выдача. Для фильтра с кнопкой «Применить» это два разных состояния. Пользователь может изменить диапазон и закрыть панель, не запуская запрос. Для мгновенного поиска их можно синхронизировать автоматически, но правило должно быть явным.
Изменение фильтра обычно сбрасывает страницу выдачи на первую. Иначе пользователь на десятой странице применит узкое условие и увидит пустой список, хотя подходящие объекты есть. Сортировку, пагинацию и выбранные условия лучше объединить в одну модель параметров запроса.
type SearchFilters = {
query: string;
categories: string[];
priceFrom: number | null;
priceTo: number | null;
sort: 'relevance' | 'price-asc';
page: number;
};
function applyFilters(draft: SearchFilters): SearchFilters {
return { ...draft, query: draft.query.trim(), page: 1 };
}Сделать URL воспроизводимым
В URL стоит сохранять применённые параметры, которые нужны для воспроизведения результата. Черновик ещё не подтверждённой формы туда попадать не обязан. Сериализацию и чтение лучше вынести в отдельные функции: тогда прямой вход по ссылке и переход кнопкой «Назад» используют одни правила.
URLSearchParams помогает читать и записывать параметры, но не проверяет их смысл. Номер страницы нужно преобразовать в допустимое целое число, сортировку — сверить со списком значений, неизвестные категории — обработать по договорённости с API. Пустые и некорректные значения должны иметь предсказуемые значения по умолчанию.
Не каждое нажатие клавиши должно создавать запись в истории. Для редактирования строки поиска часто удобнее замена текущего адреса, а для подтверждённого перехода — новая запись. В приложении следует использовать роутер фреймворка и обрабатывать восстановление состояния при навигации назад.
Не позволить старому запросу победить новый
Представим два запроса: первый отправлен для старых условий, второй — для новых. Если первый отвечает позже, он не должен перезаписать актуальную выдачу. Для поиска в RxJS подходит switchMap: при новом внешнем значении он переключается на новый внутренний поток и отписывается от предыдущего.
Отписка и физическая отмена сетевого запроса — не одно и то же. Реальная отмена зависит от источника Observable. Для Promise или собственного транспорта может понадобиться AbortController. Для сохранения данных этот шаблон тоже нельзя применять автоматически: там потерянная операция имеет другие последствия.
// query$ — поток строк из поля; search() возвращает Observable.
// Фрагмент показывает только координацию поиска.
import { of } from 'rxjs';
import {
catchError, debounceTime, distinctUntilChanged, map, switchMap,
} from 'rxjs/operators';
const results$ = query$.pipe(
map(query => query.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap(query => search(query).pipe(
map(items => ({ status: 'ready' as const, items })),
catchError(() => of({ status: 'error' as const })),
)),
);Сохранить понятный интерфейс во время поиска
Ошибка запроса не равна пустой выдаче. В первом случае нужно объяснить сбой и дать повторить действие; во втором — показать применённые условия и предложить их ослабить. Состояние загрузки тоже должно быть явным: предыдущие результаты можно оставить на экране, обозначив, что они обновляются.
Задержка ввода полезна для текста, но не обязана распространяться на все действия. Нажатие «Применить», изменение страницы и повтор после ошибки могут запускаться сразу. Если предыдущая выдача остаётся доступной, важно не выдавать её за результат новых условий.
Рабочий фильтр можно объяснить несколькими правилами: какие параметры применены, где они сохранены, какой запрос актуален и что пользователь видит в каждом состоянии. Такая модель проще и для разработки, и для человека перед экраном.
- Прямая ссылка восстанавливает параметры и выдачу.
- Назад и вперёд восстанавливают применённые условия.
- Смена фильтра сбрасывает пагинацию.
- Старый ответ не заменяет новый результат.
- Ошибка и отсутствие результатов показаны разными состояниями.