Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров9.3K

Привет хабр!

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

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

Теги:
Хабы:
+2
Комментарии12

Публикации

Истории

Работа

Ближайшие события