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(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(routeQuery); const debounced = useDebouncedQuery(input); const pushed = React.useRef(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]; }