Query from the URL
A filtered list is worth sharing. Put the query in ?q= and the link you paste into Slack opens the same list. Query is controlled only, so the URL state is yours to wire, and mantle never imports a router. This recipe is one small hook.
Two rules shape it:
- The typed text is the source of truth while you type. Bind the field straight to the URL and every keystroke is a navigation, so the URL write waits 250 ms after the last edit and replaces the history entry instead of pushing one. A clear writes at once, so an empty field never shows a filtered list.
- A URL change from outside (the back button, a link to
?q=is:shared) replaces the typed text. The hook compares the URL value with the one it saw last render and resets the text during render, not in an effect.
Type a filter below and watch the address bar.
The hook with React Router
Copy this file. It uses useSearchParams from react-router and useDebouncedCallback from @ngrok/mantle/hooks.
Filter with useDeferredValue(query), as the demo does, so a long list re-renders behind the keystroke instead of in front of it. If the list loads through TanStack Query, filter inside select and keep the query key unfiltered, so one cache entry serves every query.
Apply on Enter
Filtering on every keystroke is right for most lists. When one filter pass over the list is too slow to run while a person types, as with thousands of rows or a list that streams in, apply the query on Enter instead. Pass commitOn: "submit", and filter by appliedQuery, the query in the URL:
- Wrap the field in a
<form>. Enter with no active suggestion submits it, and the submit handler callssubmit. - Set
enterHintonQuery.KeyboardHints, so the hints say that Enter applies the filter. - Leave out
items. Each match count is one more filter pass, so the suggestions show none. - A clear still applies at once, so the clear button and the empty field agree.
Pick the mode from the list size, such as commitOn: rows.length > 2000 ? "submit" : "change". The hook reads the mode on each render, so the switch keeps the person's focus and caret.
- billing-api
- marketing-site
- design-tokens
- staging-api
- webhook-tests
The hook with nuqs
With nuqs 2.5 or later, useQueryStates owns the URL write, and its limitUrlUpdates: debounce(250) option takes the place of the debounce. One call writes q and clears page. This docs site doesn't use nuqs, so nothing on this page runs this version.