Skip to content

Add suspense support for dialogs - #209

Open
a16n-dev wants to merge 1 commit into
mainfrom
suspense-support
Open

Add suspense support for dialogs#209
a16n-dev wants to merge 1 commit into
mainfrom
suspense-support

Conversation

@a16n-dev

@a16n-dev a16n-dev commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Exposes a way for clients to use React Suspense with async dialogs.

Problem

Dialogs are rendered without a boundary of their own. If a dialog suspends — because it lazily loads its component, or reads data with use() — that suspension propagates up to the nearest <Suspense/> boundary above the <DialogOutlet/>, which is usually near the root of the app. Opening the dialog then replaces the whole page with the app-level fallback until it's ready.

API

Suspense is configured once, globally, on the <DialogProvider/> — realistically this is an all-or-nothing choice for an app, so there are no per-hook options to keep in sync:

<DialogProvider suspenseFallback={<Spinner />}>   // enables suspense
<DialogProvider suspense>                          // enabled, renders nothing while loading

Each dialog is rendered inside its own boundary, placed within IndividualDialogStateContext so a fallback can call useDialogContext() and render inside your own dialog chrome. suspense defaults to suspenseFallback !== undefined, so setting a fallback is enough.

useDialogLazy

With suspense enabled, the component loads through React.lazy: open() shows the dialog immediately and the fallback covers the chunk load, instead of awaiting the import before anything appears on screen. preload() still warms the module cache, so a preloaded dialog opens without the fallback ever showing.

With suspense disabled, the existing await-then-show behaviour is unchanged.

Notes

  • Opt-in rather than on by default. Defaulting it on would be a silent behaviour change (a suspending dialog would render nothing instead of your app spinner), and it changes error handling for useDialogLazy — a failed dynamic import currently rejects the promise from open(), but under React.lazy it throws during render and needs an error boundary. That caveat is documented; flipping the default is a reasonable v4 change.
  • Because the config is global-only, no per-dialog state was needed — show() keeps its original signature and the dialog state record is unchanged.
  • IS_REACT_ACT_ENVIRONMENT is never set in this repo, so waitFor can't flush suspense resolutions (this is also the source of the existing "not configured to support act" warnings). The new test file sets it locally and uses explicit act blocks. Moving that into setup-tests.ts repo-wide would change how the existing tests behave, so it's left alone here.

Verification

tsc --noEmit, eslint, the tsdown build, and the full test suite (19 tests, 6 new) all pass. Docs site builds — new concepts/suspense page wired into the sidebar, plus DialogProvider props and a useDialogLazy note.

🤖 Generated with Claude Code

Dialogs are currently rendered without a boundary of their own, so a
dialog that suspends - by lazily loading its component, or reading data
with use() - propagates up to the nearest boundary above the outlet,
usually near the root of the app. Opening the dialog then replaces the
whole page with the app-level fallback.

Suspense is configured once on the DialogProvider, since this is
realistically an all-or-nothing choice for an app:

    <DialogProvider suspenseFallback={<Spinner />}>

Each dialog is then rendered inside its own Suspense boundary, placed
within the individual dialog context so that a fallback can call
useDialogContext(). Setting a fallback enables suspense; pass `suspense`
explicitly to enable it without one.

This also changes useDialogLazy to load through React.lazy when suspense
is enabled, so open() shows the dialog straight away and the fallback
covers the chunk load, rather than awaiting the import before anything
appears. With suspense disabled the existing await-then-show behaviour
is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-dialog-async-docs-v2 Ready Ready Preview Aug 7, 2026 7:17am
react-dialog-async-docs-v3 Ready Ready Preview Aug 7, 2026 7:17am

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.

1 participant