Files
request-portal/src/webparts/requestPortal/shared/usePersistedQuery.ts
T
2026-08-10 18:18:42 +05:00

81 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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];
}