fix(settings): keep heading and tab strip mounted while a tab loads - #6340
Open
viktormarinho wants to merge 1 commit into
Open
fix(settings): keep heading and tab strip mounted while a tab loads#6340viktormarinho wants to merge 1 commit into
viktormarinho wants to merge 1 commit into
Conversation
Switching tabs on a tabbed settings screen (Members, Connect, Storage, Billing & AI) flashed a full-screen spinner over the whole page, taking the heading and the tab strip with it. TanStack wraps every route match in its own <Suspense> whose fallback is that route's pendingComponent, and the router sets a global defaultPendingComponent of SplashScreen (a min-h-screen centered spinner). So anything that suspended inside a settings page — the lazy route chunk on the first click of a tab, or a suspense query — replaced the entire route subtree. Page-local boundaries never got a chance for the chunk case, and Members/Roles had theirs above the subnav anyway. Add SettingsGroupPage, the shared shell for tabbed settings screens: it renders the heading and tab strip above an ErrorBoundary/Suspense pair, so page data only ever swaps the content region for a skeleton. Neither hook behind the tab strip suspends (useCapabilities and useOwnedSites are plain useQuery), so the chrome is safe outside the boundary. Pair it with settingsGroupPendingComponent(group) as the route-level pendingComponent on the eight tabbed routes that render a component, overriding the global SplashScreen so a cold chunk load paints the same chrome plus skeleton. Roles keeps a plain spinner when ?role= is set — the role editor is a full-bleed screen with its own heading and shouldn't get the tab strip. Untabbed settings pages are unchanged. Cover it in packages/e2e/tests/settings-navigation.spec.ts: hold the Buckets tab's FILE_CONFIG_LIST call open and assert the heading and tabs are still on screen while the content skeleton shows.
viktormarinho
force-pushed
the
t3code/fix-settings-tab-loading
branch
from
August 20, 2026 17:35
a7aba37 to
9717e6e
Compare
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.
Summary
Switching tabs on a tabbed settings screen (Billing, Members, Connect, Storage) flashed a full-screen spinner over the whole page — the heading and the tab strip disappeared along with the content.
Root cause is two boundaries, not one:
<Suspense fallback={pendingComponent}>(Match.tsx→ResolvedSuspenseBoundary), androuter.tsxsets a globaldefaultPendingComponent: SplashScreen— amin-h-screencentered spinner. So anything that suspends inside a settings page (the lazy route chunk on the first click of a tab, or a suspense query) replaces the entire route subtree. The page-local<Suspense>boundaries some pages already had never got a chance for the chunk case, because the chunk suspends before the component renders at all.<Suspense>above the subnav, souseMembers()blanked the chrome even once the chunk was cached.Changes
New
apps/web/src/components/settings/settings-group-page.tsxowns both halves:SettingsGroupPage— the shared shell for tabbed settings screens. RendersPage → Page.Content → Page.Body → SettingsPagewith the heading + tab strip above anErrorBoundary/Suspensepair, so page data only ever swaps the content region for a skeleton. Neither hook behind the tab strip suspends (useCapabilities,useOwnedSitesare plainuseQuery), so the chrome is safe outside the boundary — and safe to render from inside a fallback.settingsGroupPendingComponent(group)— route-levelpendingComponent, wired onto all 9 tabbed routes inrouter.tsx. Overrides the globalSplashScreenso a cold chunk load paints the same chrome + skeleton.All 9 grouped pages now go through it:
connect/api-keys,members/roles,billing/ai-providers/infra-billing,buckets/synced-repos. Net −87 lines (each page dropped its ownPage/SettingsPage/Suspense/ErrorBoundaryscaffolding).Roles keeps a plain spinner when
?role=is set — the role editor is a full-bleed screen with its own heading and shouldn't get the tab strip; that branch is decided from the URL, which never suspends.Untabbed settings pages (General, Secrets, Store, Monitor, Tasks) are unchanged — no chrome to preserve.
Affected areas
/$org/settings/{connect,api-keys,members,roles,billing,ai-providers,infra-billing,buckets,synced-repos}Behavior change worth flagging
On the Members page,
JoinRequestsSectionused to render above the "Members" heading; it's now the first item under the tab strip. Happy to add a slot above the subnav if the original placement was deliberate.Testing
bun run --cwd=apps/web check— cleanbun run lint— no new findingsbunx knip— cleanbun run fmt— cleanNot exercised in a browser: no dev server was up in this worktree and booting the stack needs an
.envplus a seeded org. Worth a manual pass clicking through each tab group (including a hard reload so the chunks are cold) before merge.Summary by cubic
Keep the settings heading and tab strip visible while a tab loads. Previously, any suspense replaced the entire page with a full-screen spinner; now only the content area shows a skeleton.
SettingsGroupPageto render the heading and tab strip above anErrorBoundary/Suspensepair so only the content region swaps during data load.settingsGroupPendingComponent(group)on tabbed routes to override the globalSplashScreen, keeping the same chrome + skeleton during cold chunk loads.connect/api-keys,members/roles,billing/ai-providers/infra-billing,buckets/synced-repos. Untabbed settings pages are unchanged.?role=is set, keep a centered spinner (full-bleed editor without tabs).JoinRequestsSectionnow renders under the tab strip.data-slot="settings-heading"anddata-testid="settings-content-loading"for assertions.Written for commit 9717e6e. Summary will update on new commits.