Add suspense support for dialogs - #209
Open
a16n-dev wants to merge 1 commit into
Open
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:Each dialog is rendered inside its own boundary, placed within
IndividualDialogStateContextso a fallback can calluseDialogContext()and render inside your own dialog chrome.suspensedefaults tosuspenseFallback !== undefined, so setting a fallback is enough.useDialogLazyWith 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
useDialogLazy— a failed dynamic import currently rejects the promise fromopen(), but underReact.lazyit throws during render and needs an error boundary. That caveat is documented; flipping the default is a reasonable v4 change.show()keeps its original signature and the dialog state record is unchanged.IS_REACT_ACT_ENVIRONMENTis never set in this repo, sowaitForcan'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 explicitactblocks. Moving that intosetup-tests.tsrepo-wide would change how the existing tests behave, so it's left alone here.Verification
tsc --noEmit,eslint, thetsdownbuild, and the full test suite (19 tests, 6 new) all pass. Docs site builds — newconcepts/suspensepage wired into the sidebar, plusDialogProviderprops and auseDialogLazynote.🤖 Generated with Claude Code