Пишем кастомный Хук для фильтров используя параметры страницы(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 location = useLocation();
	const initialFilter = getObjectFromQueryString(location.search);
	const [filter, setFilter] =  useState(initialFilter);

  return [filter];
};

Итак, при открытии страницы из URL мы будем получать значения фильтров. Теперь нам нужно, чтобы при изменении фильтра у нас менялись значения query параметров. Напишем функцию, которая будет устанавливать новое значение.

  const setSearchQuery = (filter: Filter) => {
      const search = getQueryStringFromObject(filter);

      history.replace({ search });
  };

Использование replace или push зависит от требований, если вы хотите записывать в историю все изменения фильтра то можно использовать push, в моем случае достаточно записать только последнее. Подробнее о методах можно посмотреть в документации React Router

Теперь напишем функции, которые будем передавать в качестве callback function из нашего кастомного hook. Нам потребуется функция для изменения фильтра

const onChangeFilter = (fieldName: string) => (value: string) => {
   const newFilter = { ...filter, [fieldName]: value };
 
   setFilter(newFilter);
   setQueryParams(newFilter);
};

Функция для удаления значения из фильтра

const onClearFilter = (fieldName: string) => () => {
   const newFilter = omit(filter, fieldName);

   setFilter(newFilter);
   setQueryParams(newFilter);
};

В качестве вспомогательной функции, использовали omit из lodash, которая будет удалять свойство из объекта

Cоберём всё это в наш кастомный hook, но перед этим сделаем несколько изменений для того, чтобы он стал универсальным для разных фильтров.

  • Обернем наши callback function в useCallback для мемоизации.

  • Добавим типизацию в нашем кастомный hook для передачи правильных типов в компоненты.

  • Добавим, в качестве необязательных параметров, две функции для обработки фильтра и query строки для более сложных фильтров

После выполнения данных действий, получим описание типов, которые будут передаваться их нашего 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 initialFilter = getFilterQuery 
		? getFilterQuery(search) 
		: getObjectFromQueryString(search);

  const [filter, setFilter] = useState<T>(initialFilter);

  const setSearchQuery = (filter: T) => {
      const search = getSearchQuery 
      	? getSearchQuery(filter) 
      	: getQueryStringFromObject(filter).toString();

      history.replace({ search });
  };

  const onChangeFilter = useCallback(
    (fieldName: string) => (value: string) => {
      const newFilter = { ...filter, [fieldName]: value };

      setFilter(newFilter);
      setSearchQuery(newFilter);
    },
    [filter, setSearchQuery, setFilter]
  );

  const onClearFilter = useCallback(
    (fieldName: string) => () => {
      const newFilter = omit(filter, fieldName);

      setFilter(newFilter);
      setSearchQuery(newFilter);
    },
    [filter, setSearchQuery, setFilter]
  );

  return [filter, onChangeFilter, onClearFilter];
}

Функции которые передаются в качестве параметров в 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, onChangeFilter, onClearFilter] = 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/post/590721/


Интересные статьи

Интересные статьи

Попытаюсь объять необъятное: сделать обзор предложить классификацию публикаций на Хабре. Начну с примеров.
SWAP (своп) — это механизм виртуальной памяти, при котором часть данных из оперативной памяти (ОЗУ) перемещается на хранение на HDD (жёсткий диск), SSD (твёрдотельный накоп...
Доброго времени суток, друзья! Предисловие Однажды веб серфинг привел меня к этому. Позже обнаружил статью про то, как это работает. Казалось бы, ничего особенного — Пикачу, нарис...
Предыстория Так произошло, что сервере был атакован вирусом шифровальщиком, который по "счастливой случайности", частично отставил не тронутыми файлы .ibd (файлы сырых данных innodb ...
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?