Привет хабр!
Недавно на одном из проектов, появилась потребность добавить на страницу фильтры, которые не должны терять данные при перезагрузке страницы и автоматически устанавливать значения при переходе по ссылке. Логичным решением стало использовать параметры url страницы в качестве источника данных.
Но если таких страниц на вашем проекте много, а количество фильтров может отличаться, то встает вопрос, можно ли как то сделать универсальную функцию в которой будет инкапсулироваться основные действия, ведь не хочется для каждой страницы заново писать код.
Раз уж так сейчас стало популярным использовать и писать свои hooks, давайте попробуем написать свой кастомный hook, который будет удовлетворять этим требованиям. Но сначала немного разберемся с понятиями.
Параметры в URL — это последовательность символов, расположенных после адреса. Для их отделения от основного URL используется знак вопроса. Каждый параметр представляет собой пару ключ=значение. Для отделения пар друг от друга используется знак «&».

Например у нас есть страница со списком сотрудников, в которой есть query параметры.
https://example-web-page/employees?name=Bob&surname=Jordan&position=engineer
В процессе написания hook будем использовать React, React Router, TypeScript
Начнем с простого, опишем наш фильтр.
type Filter = { name?: string; surname?: string; position?: string; };
Далее опишем функцию, которая будет из фильтра делать query строку
const getQueryStringFromObject = (filter: Filter) => { return new URLSearchParams(filter).toString(); };
URLSearchParams не поддерживается старыми браузерами и IE. Если требуется поддержка, то используйте методы stringify и parse из query-string, подробнее о нем можно узнать здесь
И наоборот из query строки будем делать фильтр.
export const getObjectFromQueryString = (search: string) => { const paramsEntries = new URLSearchParams(search).entries(); return Object.fromEntries(paramsEntries); };
Далее будем использовать hooks, которые нам предоставляет React Router: useHistory, useLocation. Подробнее о них можно почитать в документации
Начнем собирать наш кастомный hook
export function useFiltersQuery() { const { search } = useLocation(); const filter = getObjectFromQueryString(search); return [filter]; }
Итак, при открытии страницы из URL мы будем получать значения фильтров. Теперь нам нужно, чтобы при изменении фильтра у нас менялись значения query параметров. Напишем функцию, которая будет устанавливать новое значение.
const setSearchQuery = (filter: Filter) => { const search = getQueryStringFromObject(filter); history.replace({ search }); };
Использование replace или push зависит от требований, если вы хотите записывать в историю все изменения фильтра то можно использовать push, в моем случае достаточно записать только последнее. Подробнее о методах можно посмотреть в документации React Router
Теперь напишем функции, которые будем передавать в качестве callback function из нашего кастомного hook. Нам потребуется функция для изменения фильтра
const сhangeFilter = (fieldName: string) => (value: string) => { const newFilter = { ...filter, [fieldName]: value }; setQueryParams(newFilter); };
Функция для удаления значения из фильтра
const сlearFilter = (fieldName: string) => () => { const newFilter = omit(filter, fieldName); setQueryParams(newFilter); };
В качестве вспомогательной функции, использовали omit из lodash, которая будет удалять свойство из объекта
Cоберём всё это в наш кастомный hook, но перед этим сделаем несколько изменений для того, чтобы он стал универсальным для разных фильтров.
Добавим типизацию в нашем кастомный hook для передачи правильных типов в компоненты.
Добавим, в качестве необязательных параметров, две функции для обработки фильтра и query строки для более сложных фильтров
Обернем функции в useCallback для мемоизации ссылок и useMemo для мемоизации фильтра
После выполнения данных действий, получим описание типов, которые будут передаваться их нашего hook.
type useFilterQueryTypes<T> = [ T, (fieldName: string) => (value: string) => void, (fieldName: string) => () => void ];
И сам кастомных hook
export function useFilterQuery<T extends object>( getFilterQuery?: (query: string) => T, getSearchQuery?: (filter: T) => string ): useFilterQueryTypes<T> { const { search } = useLocation(); const history = useHistory(); const filter = useMemo(() => // используем функцию переданную через параметры или дефолтную (getFilterQuery ? getFilterQuery(search) : getObjectFromQueryString(search)), [search, getFilterQuery] ); const setSearchQuery = useCallback((filter: T) => { // используем функцию переданную через параметр или дефолтную const search = getSearchQuery ? getSearchQuery(filter) : getQueryStringFromObject(filter).toString(); history.replace({ search }); }, [history, getSearchQuery] ); const сhangeFilter = useCallback((fieldName: string) => (value: string) => { const newFilter = { ...filter, [fieldName]: value }; setSearchQuery(newFilter); }, [filter, setSearchQuery] ); const сlearFilter = useCallback((fieldName: string) => () => { const newFilter = omit(filter, fieldName); setSearchQuery(newFilter); }, [filter, setSearchQuery] ); // возвращаем сам фильтр и две функции для его изменения return [filter, сhangeFilter, сlearFilter]; }
Функции которые передаются в качестве параметров в useFiltersQuery, можно было не передавать в данном примере, но для более сложных фильтров это потребуется сделать.
Пример более сложного фильтра
Например если нам потребуется сделать из одного query параметра несколько фильтров, или как в примере ниже, получить данные по идентификатору. В этом варианте для примера используем методы stringify и parse из query-string для случая когда требуется поддержка старых браузеров.
type Contact = { id: string; phone: number; email: string; }; type Filter = { phone: number; email: string; }; const getContactsFilter = (contacts : Contact[]) => (search: string) => { const { id } = qs.parse(search); const { phone, email } = contacts.find(contact => contact.id === id) || {}; return { phone, email, }; }; const getSearchQuery = (filter: Filters) => { const { phone, email } = filter; const contactId = someExampleSearchFunction(phone, email) return qs.stringify(contactId); };
Посмотрим как его можно вызывать из компонента
const Employees: React.FC = ({ list }: Props) => { const [filter, сhangeFilter, сlearFilter] = useFiltersQuery<Filters>(); return ( <> <EmployesFilters filter={filter} onChangeFilter={onChangeFilter} onClearFilter={onClearFilter} /> <EmployeesList list={list} filter={filter} /> </> ); };
В случае усложнения обработки фильтров нужно будет прокидывать эти функции в useQueryFilters в качестве параметров.
Пример вызовов функций кастомного hook из самого фильтра, это может быть как dropdown так и input для поиска
Пример вызовов функций кастомного hook из самого фильтра
Это может быть как dropdown так и input для поиска, так и другие варианты
<Filter selectedItem={filter.name} items={names} onChange={onChangeFilter("name")} onClear={onClearFilter("name")} /> <SearchInput value={filter.position} onChange={onChangeFilter("position")} onClear={onClearFilter("position")} />
Вот и все, мы получили наш hook, который можно использовать для фильтров на разных страницах. В написании hook, нет ничего сложного. Ведь любой hook – это такая же функция, которая требует лишь несколько правил:
Начало функции должно начинаться со слова use, это говорит о том, что это hook.
Выполнять hooks следует в самом верху иерархии функционального компонента React (нельзя вызывать hooks в условиях и циклах)
Вызывать hooks можно только в React функциях или функциональных компонентах React или вызывать hooks из кастомных hooks (как сделано в нашем примере)
Более подробнее о правилах, можно посмотреть в документации React. Надеюсь статья была полезной для вас.
Удачного кодинга, друзья! Всем Пока
