81 lines
5.0 KiB
TypeScript
81 lines
5.0 KiB
TypeScript
import * as React from 'react';
|
||
|
||
// Задержка перед тем, как введённый текст уйдёт в запрос. Без неё поиск
|
||
// срабатывает на КАЖДОЕ нажатие клавиши: «согласование» — это 12 полных циклов
|
||
// (Search + дочитывание через REST по каждому типу заявок), из которых нужен
|
||
// только последний. Заодно снимает часть нагрузки, из-за которой SharePoint
|
||
// начинает throttling'ить (429).
|
||
const SEARCH_DEBOUNCE_MS = 450;
|
||
|
||
// Значение, отстающее от переданного на SEARCH_DEBOUNCE_MS: в поле ввода текст
|
||
// меняется сразу (без «залипания»), а в запрос уходит только когда пользователь
|
||
// перестал печатать. Пустая строка проходит без задержки — очистка поиска
|
||
// должна возвращать полный список мгновенно.
|
||
export function useDebouncedQuery(value: string, delayMs: number = SEARCH_DEBOUNCE_MS): string {
|
||
const [debounced, setDebounced] = React.useState<string>(value);
|
||
|
||
React.useEffect(() => {
|
||
if (!value) {
|
||
setDebounced('');
|
||
return undefined;
|
||
}
|
||
const timer = window.setTimeout(() => setDebounced(value), delayMs);
|
||
return () => window.clearTimeout(timer);
|
||
}, [value, delayMs]);
|
||
|
||
return debounced;
|
||
}
|
||
|
||
/**
|
||
* Связывает поле поиска с маршрутом: в поле текст меняется мгновенно, а в
|
||
* маршрут (и, значит, в адрес) уходит только после паузы в наборе.
|
||
*
|
||
* Раньше текст хранился прямо здесь, в localStorage (usePersistedQuery). Теперь
|
||
* источник правды — маршрут: так поиск попадает в ссылку и работает кнопка
|
||
* «назад», а запоминание между визитами делает persistQuery в shared/routing.ts.
|
||
*
|
||
* Неочевидное место — ref `pushed`: в нём лежит текст, на котором поле и
|
||
* маршрут в последний раз сошлись. Он нужен в ОБЕ стороны.
|
||
*
|
||
* Вниз (маршрут → поле): без него собственное же изменение маршрута
|
||
* возвращалось бы обратно в поле и затирало текст, набранный за время паузы:
|
||
* пользователь печатает «абвг», через 450 мс в маршрут уходит «абв», маршрут
|
||
* меняется, и обратная синхронизация вернула бы в поле «абв», съев «г».
|
||
* Поэтому поле перечитывается только от ВНЕШНИХ смен (другой раздел, «назад»).
|
||
*
|
||
* Вверх (поле → маршрут): сравнивать `debounced` нужно именно с `pushed`, а НЕ
|
||
* с текущим routeQuery. RequestsTableView — один экземпляр на все таблицы
|
||
* («На согласовании», «Мои заявки» и т.д.), и при переключении раздела меняется
|
||
* не поле, а routeQuery. Со сравнением по routeQuery это выглядело как «текст
|
||
* изменился» — и запрос ПРЕДЫДУЩЕЙ таблицы уезжал в новую (и там же
|
||
* сохранялся), из-за чего один и тот же поиск оказывался во всех разделах.
|
||
*/
|
||
export function useRoutedQuery(
|
||
routeQuery: string,
|
||
onQueryChange: (query: string) => void
|
||
): [string, (value: string) => void] {
|
||
const [input, setInput] = React.useState<string>(routeQuery);
|
||
const debounced = useDebouncedQuery(input);
|
||
const pushed = React.useRef<string>(routeQuery);
|
||
|
||
React.useEffect(() => {
|
||
if (debounced === pushed.current) return;
|
||
pushed.current = debounced;
|
||
onQueryChange(debounced);
|
||
// onQueryChange пересоздаётся на каждый рендер родителя — в зависимостях
|
||
// он гонял бы эффект вхолостую; важен только сам факт смены debounced.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [debounced]);
|
||
|
||
React.useEffect(() => {
|
||
if (routeQuery === pushed.current) return;
|
||
// Пришло извне — принимаем как новую точку схождения. Иначе догоняющий
|
||
// debounced (в поле уже новый текст) тут же счёл бы себя правкой
|
||
// пользователя и отправил бы этот же текст обратно в маршрут.
|
||
pushed.current = routeQuery;
|
||
setInput(routeQuery);
|
||
}, [routeQuery]);
|
||
|
||
return [input, setInput];
|
||
}
|