у поиска нормальный роутинг

This commit is contained in:
gorbenko.pavel
2026-08-10 18:18:42 +05:00
parent 7ef0ee6245
commit 67c8657618
7 changed files with 365 additions and 115 deletions
@@ -1,54 +1,5 @@
import * as React from 'react';
// Текст поиска переживает перезагрузку страницы (localStorage), но не более
// того — это просто «продолжить с того же места», а не синхронизация между
// вкладками/устройствами. key должен быть уникальным на раздел (см. вызовы в
// RequestsTableView/SearchView), иначе значения разных таблиц перепишут друг друга.
function readStored(storageKey: string): string {
try {
return window.localStorage.getItem(storageKey) || '';
} catch {
return '';
}
}
export function usePersistedQuery(key: string): [string, (value: string) => void] {
const storageKey = `rp-search-query:${key}`;
const [value, setValue] = React.useState<string>(() => readStored(storageKey));
// Компонент может продолжать жить, когда key поменялся: RequestsTableView —
// это один и тот же элемент для вкладок «На согласовании» и «Мои заявки»,
// меняется только проп kind. Инициализатор useState при этом повторно НЕ
// срабатывает, поэтому без перечитывания текст из одной вкладки оставался бы
// на экране во второй и записывался уже под её ключ — выглядело бы так, будто
// ключ у них общий.
const prevKey = React.useRef<string>(storageKey);
React.useEffect(() => {
if (prevKey.current === storageKey) return;
prevKey.current = storageKey;
setValue(readStored(storageKey));
}, [storageKey]);
const setPersisted = React.useCallback(
(next: string): void => {
setValue(next);
try {
if (next) {
window.localStorage.setItem(storageKey, next);
} else {
window.localStorage.removeItem(storageKey);
}
} catch {
// localStorage недоступен (приватный режим и т.п.) — просто не сохраняем.
}
},
[storageKey]
);
return [value, setPersisted];
}
// Задержка перед тем, как введённый текст уйдёт в запрос. Без неё поиск
// срабатывает на КАЖДОЕ нажатие клавиши: «согласование» — это 12 полных циклов
// (Search + дочитывание через REST по каждому типу заявок), из которых нужен
@@ -74,3 +25,56 @@ export function useDebouncedQuery(value: string, delayMs: number = SEARCH_DEBOUN
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];
}