у поиска нормальный роутинг
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user