Search experience
Complete search pattern, from the field to the results.
Draft
Specified, not implemented yet. The field is SearchBar; the rest is screen logic. The API may change.
SearchBar is only the field. The search experience is everything around it: what the screen shows before the user types, while they type, and when nothing matches.
Tap the field: the list gives way to recent searches. Type a city. The request leaves after a short pause, not on every key, and the footer counts both. The fake server answers with a random delay, so type fast and watch stale ignored go up. Open a result to save the query. Try xyz for the no-results state, and Cancel to go back.
The screen, frame by frame:
Empty field: recent searches, most recent first.
A 250ms pause sends the request. The spinner takes the icon’s place.
Matches, with the typed part in bold.
Repeat the query, offer a way out.
Recent searches
What the screen shows when the field is focused and still empty.
- List the last queries, most recent first, as tappable rows built with Item.
- Tapping a row runs that query again.
- Let the user remove one entry, and clear the whole list.
Search history
Where recent searches come from.
- Save a query when the user opens a result or submits, not on every keystroke.
- Store it on the device and cap the list around 10 entries.
- Move a repeated query to the top instead of adding it twice.
Debounce
Wait for a short pause in typing before sending the request, around 250 ms.
- Show the
loadingstate of SearchBar while the request runs. - Ignore responses to older queries, so results never flash back to a previous one.
- A local list can filter on every keystroke, without debounce.
Cancel
Cancel leaves search mode.
- It clears the field, closes the keyboard and returns to the previous screen.
- The clear button inside the field only empties the query and keeps the user in search.
No results
The query matched nothing.
- Repeat the query in the title:
No results for "lsbon". - Offer a way out with Clear search.
- No “create” action. The user was looking for something.
See No results in async content state.
Implementation
const [query, setQuery] = useState("");
const debounced = useDebouncedValue(query.trim(), 250);
// The query key changes with the text, so an older response never lands on a newer query.
const results = useQuery({
queryKey: ["search", debounced],
queryFn: () => api.search(debounced),
enabled: !!debounced,
});
<SearchBar
value={query}
onChangeText={setQuery}
loading={results.isFetching}
onCancel={navigation.goBack}
/>;
{
!query ? (
<RecentSearches onPress={setQuery} />
) : results.data?.length === 0 ? (
<NoResults query={debounced} onClear={() => setQuery("")} />
) : (
<ResultList data={results.data} />
);
}Accessibility
- Announce the number of results when they arrive (“12 results”), not while typing.
- The clear button and each “remove” button in recents have their own label.
- Cancel is reachable with the system back gesture too.
Related
- SearchBar
- Async content state
- Empty, for the no-results state
- Chip, for filters