Reference solution
The findings below are ordered by priority.
Findings
1. Older responses can replace results for the newest query
Location: src/customer-search.tsx:13-24
Requests have no abort signal or generation guard. If an earlier search responds after a later one, its continuation overwrites the current results and also clears loading while newer work may still be pending.
Create an AbortController per eligible effect, pass its signal, abort it in cleanup, and ensure only that effect may commit state.
2. Timers survive query changes and unmounting
Location: src/customer-search.tsx:16-24
The effect returns no cleanup for setTimeout. Fast typing schedules one request for every intermediate query rather than one after inactivity, and an unmounted component can still start network work later.
Retain the timer handle and clear it in the effect cleanup.
3. Empty and one-character input still searches
Location: src/customer-search.tsx:13-24
Every query sets loading and schedules a request, without trimming or enforcing the two-character threshold. Clearing the field also leaves old results visible until an unnecessary request completes.
Derive the trimmed query at the start of the effect; for an ineligible value, synchronously clear results and loading and return without a timer.
4. Query text is interpolated without URL encoding
Location: src/search-client.ts:6-10
Spaces, ampersands, equals signs, and non-ASCII text can change or corrupt the request query because the value is inserted directly into the URL. For example, a&role=admin becomes two parameters.
Build the search string with URLSearchParams or otherwise encode the trimmed query as one parameter value.
5. Request failures become unhandled rejections with no error UI
Location: src/customer-search.tsx:17-23
The promise chain has no rejection handler. A network or HTTP failure still runs finally, but it produces an unhandled rejection and never renders the required message.
Catch non-abort failures, set an error state while retaining the previous results, and clear that error when a later eligible request begins or succeeds.
Reasonable non-findings
- Keeping the last successful list while a new eligible query loads is permitted.
- Keying result elements by their stable server ID is correct.
- A 300 ms timeout matches the stated debounce interval.