Skip to content

feat(combobox): built-in error state for async dropdowns#389

Merged
erickteowarang merged 6 commits into
mainfrom
1491-adjust-combobox-error-state
Jul 16, 2026
Merged

feat(combobox): built-in error state for async dropdowns#389
erickteowarang merged 6 commits into
mainfrom
1491-adjust-combobox-error-state

Conversation

@erickteowarang

@erickteowarang erickteowarang commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements a first-class error state for Combobox.Async, Autocomplete.Async, and Select.Async, closing tailor-inc/platform-planning#1491.

Previously a failed fetch rendered identically to a successful empty fetch ("No results."), so users couldn't tell "nothing matches" from "the request failed" — and the documented contract pushed ~15 lines of fiddly toast/abort/de-dupe boilerplate onto every consumer.

Now, when a fetcher throws/rejects, the component renders an inline error state in the popover — "Couldn't load results." + a Retry button that re-runs the last fetch — instead of the misleading empty state. No consumer code required.

Screencaps

Combobox/Autocomplete

Screenshot 2026-07-16 at 2 39 53 pm

Select

Screenshot 2026-07-16 at 2 39 44 pm

What changed

  • Shared useAsyncItems hook (Combobox + Autocomplete): adds retry() (re-runs the last query, no debounce) and an onFetchError option. The two sharp edges are handled centrally — aborted/superseded requests are ignored, and onFetchError fires once per outage (on entry into the error state, re-arming after the next success) so typing during an outage doesn't stack announcements.
  • New AsyncErrorState component + resolveAsyncContent helper — the shared inline error UI (focus-preserving Retry click) and the loading→error→default content picker.
  • New props on each .Async component: errorText (default "Couldn't load results."), retryText (default "Retry"), onFetchError.
  • Select.useAsync is a separate implementation, so it got matching retry / onFetchError / once-per-outage plumbing.
  • Parts consumers (Combobox.useAsync etc.) also gain error + retry and accept onFetchError.
  • Docs, tests, changeset (minor), and an "Error + retry" example added to each async card in the nextjs demo app.

Testing

  • type-check, lint, fmt:check clean across core + example.
  • 1363 tests pass (+8 new: hook retry / once-per-outage / abort-safety / error-clears-on-success, and per-component error-renders / Retry-refetches / once-per-outage). No snapshot changes.
  • Verified live in the nextjs demo app across all three components.

🤖 Generated with Claude Code

Combobox.Async, Autocomplete.Async, and Select.Async now render an inline
error state (message + Retry) in the popover when the fetcher throws/rejects,
instead of silently falling back to the misleading "No results." empty state.

- Shared useAsyncItems hook gains retry() and an onFetchError option that
  fires once per outage; aborted/superseded requests are ignored centrally.
- New AsyncErrorState component + resolveAsyncContent helper.
- New props on each .Async component: errorText, retryText, onFetchError.
- Select.useAsync (separate impl) gets matching retry/onFetchError plumbing.
- Docs, tests, changeset, and an example in the nextjs demo app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@erickteowarang
erickteowarang requested a review from a team as a code owner July 16, 2026 04:18
@erickteowarang

Copy link
Copy Markdown
Contributor Author

@copilot can you do a review of this PR and let me know if you find any gaps?

…Error test

Co-authored-by: erickteowarang <30395423+erickteowarang@users.noreply.github.com>

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@copilot can you do a review of this PR and let me know if you find any gaps?

Overall the implementation is solid — the hook logic, per-component wiring, docs, and changeset all look consistent. I ran type-check, lint, and 1363 existing tests (all green). Here's what I found and fixed:


Fixed (commit fix: update stale JSDoc examples and add missing autocomplete onFetchError test)

  1. Stale useAsync JSDoc examples in combobox.tsx and autocomplete.tsx — both Parts-level useAsync examples still showed the old loading ? "Loading..." : "No results." pattern with no mention of the newly-exposed error and retry return values. Updated to show the full ternary so Parts consumers know what to do on error.

  2. Missing onFetchError once-per-outage test for Autocomplete.Asynccombobox-standalone.test.tsx and select-standalone.test.tsx both have this test; autocomplete-standalone.test.tsx was missing it. Added the equivalent test (+1 test, 1364 total passing).


Observations (no changes needed)

  • Logic correctness: The inErrorStateRef once-per-outage guard, abort-signal filtering, and lastFetchQueryRef replay all look correct. The once-per-outage behavior is covered by the useAsync unit tests in combobox.test.tsx which exercise the shared useAsyncItems hook.
  • resolveAsyncContent priority: loading correctly takes precedence over error, so a retry immediately shows the loading state rather than leaving the error visible.
  • Focus preservation: onMouseDown={(e) => e.preventDefault()} on the Retry button correctly keeps focus on the input/trigger so the popover stays open during retry.
  • Select.useAsync retry safety: runFetch aborts the prior controller before creating a new one; the !controller.signal.aborted guard in .then/.catch/.finally prevents stale state updates if the dropdown closes mid-retry.
  • AsyncErrorState not exported from index.ts: This is intentional — the component is an internal rendering detail. Parts consumers get error and retry from useAsync and can build their own error UI.

@IzumiSy

IzumiSy commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

/review

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

API Design Review completed successfully!

API design review complete for PR #389 — no issues to flag. All new public props (errorText, retryText, onFetchError) are optional with defaults; AsyncFetcher type union was pre-existing; return type additions (retry) are purely additive. Verdict: Approve.

@IzumiSy IzumiSy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. Thanks!

@erickteowarang
erickteowarang merged commit 73d0c0a into main Jul 16, 2026
5 checks passed
@erickteowarang
erickteowarang deleted the 1491-adjust-combobox-error-state branch July 16, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants