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