Пишем кастомный Хук для фильтров используя параметры страницы (query string)

—

от автора

Привет хабр!

Недавно на одном из проектов, появилась потребность добавить на страницу фильтры, которые не должны терять данные при перезагрузке страницы и автоматически устанавливать значения при переходе по ссылке. Логичным решением стало использовать параметры 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. Надеюсь статья была полезной для вас.

Удачного кодинга, друзья! Всем Пока


ссылка на оригинал статьи https://habr.com/ru/articles/590721/


Комментарии

Добавить комментарий

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