Problem
As a consequence of #146 (persist list table sort, columns, and filters in localStorage), controlled text fields — especially list name search inputs — can reset while the operator is still typing.
When the user enters or deletes text quickly, especially on a list page that triggers a large re-render (filtered DataTable, distance sort, etc.), the field value is briefly overwritten from persisted/URL state and the keystroke is lost.
Typical symptom: backspacing to clear or shorten a search string, but deleted characters immediately reappear.
Suspected root cause
List search is a fully controlled input bound to URL search params via useChannelListQuery and useListNameQuery:
nameFilter ← searchParams.get('q')
setNameFilter → setSearchParams on every keystroke and debouncedMerge*ListPrefs (300 ms) to localStorage
On mount / location.search change, a hydration useEffect restores prefs from localStorage when the URL has no relevant params (hasEntityListUrlParams / hasChannelListUrlParams).
Race when clearing or rapidly editing search:
- Operator deletes the last character →
q is removed from the URL.
- Hydration effect sees no URL params and reads stale
localStorage (debounced write has not flushed yet).
- Effect calls
setSearchParams with the old q value → text reappears.
Heavy list re-renders (channels table + filters) may widen the window where URL/state and the input are briefly out of sync during rapid entry.
Possible fix
Debounce the filter input at the UI boundary — keep a short-lived local draft value in the text field and only commit to URL / localStorage after input settles (few hundred ms). Optionally show a subtle pending/spinner state while debouncing.
Alternative / complementary:
- Do not re-hydrate from
localStorage while the operator is actively editing (track focus / dirty flag).
- Flush debounced prefs synchronously before hydration can read stale storage.
- Hydrate only on initial route visit, not on every
location.search change when the change originated from the same hook.
Affected
src/hooks/useChannelListQuery.ts
src/hooks/useListNameQuery.ts
src/lib/listPrefs/storage.ts — debouncedMerge*ListPrefs (300 ms)
src/components/SectionNav/sections/ChannelsListSectionNav.tsx — search TextInput
- Entity list routes / section nav using
useListNameQuery (zones, contacts, talk groups, RX group lists)
src/components/ui/DataTable.tsx — toolbar search prop if wired the same way
Repro (manual)
- Open Channels (or Zones / Contacts) with a persisted name search from a prior visit (#146).
- Click the search field and rapidly backspace to clear the filter (or edit several characters quickly).
- Actual: deleted text snaps back; typing feels "fighting" the field.
- Expected: field reflects keystrokes until input settles; persisted prefs update after debounce without clobbering active edits.
Repeat on Channels with distance filter / large channel count to reproduce under heavy table updates.
Test plan
Related
- Regression from #146 — persist list table sort, columns and filters in
localStorage
- Follows #138 — standardised datatables (search in section nav / toolbar)
Problem
As a consequence of #146 (persist list table sort, columns, and filters in
localStorage), controlled text fields — especially list name search inputs — can reset while the operator is still typing.When the user enters or deletes text quickly, especially on a list page that triggers a large re-render (filtered
DataTable, distance sort, etc.), the field value is briefly overwritten from persisted/URL state and the keystroke is lost.Typical symptom: backspacing to clear or shorten a search string, but deleted characters immediately reappear.
Suspected root cause
List search is a fully controlled input bound to URL search params via
useChannelListQueryanduseListNameQuery:nameFilter←searchParams.get('q')setNameFilter→setSearchParamson every keystroke anddebouncedMerge*ListPrefs(300 ms) tolocalStorageOn mount /
location.searchchange, a hydrationuseEffectrestores prefs fromlocalStoragewhen the URL has no relevant params (hasEntityListUrlParams/hasChannelListUrlParams).Race when clearing or rapidly editing search:
qis removed from the URL.localStorage(debounced write has not flushed yet).setSearchParamswith the oldqvalue → text reappears.Heavy list re-renders (channels table + filters) may widen the window where URL/state and the input are briefly out of sync during rapid entry.
Possible fix
Debounce the filter input at the UI boundary — keep a short-lived local draft value in the text field and only commit to URL /
localStorageafter input settles (few hundred ms). Optionally show a subtle pending/spinner state while debouncing.Alternative / complementary:
localStoragewhile the operator is actively editing (track focus / dirty flag).location.searchchange when the change originated from the same hook.Affected
src/hooks/useChannelListQuery.tssrc/hooks/useListNameQuery.tssrc/lib/listPrefs/storage.ts—debouncedMerge*ListPrefs(300 ms)src/components/SectionNav/sections/ChannelsListSectionNav.tsx— searchTextInputuseListNameQuery(zones, contacts, talk groups, RX group lists)src/components/ui/DataTable.tsx— toolbar search prop if wired the same wayRepro (manual)
Repeat on Channels with distance filter / large channel count to reproduce under heavy table updates.
Test plan
setNameFilter/ clear does not re-hydrate stalelocalStorageqover the in-progress value.Related
localStorage