Unblock the host room page after room creation - #32
Conversation
Close the create-room dialog before navigating so its overlay and the submit spinner cannot outlive the mutation. Add a loading.tsx for /host/[code] so the route transition renders the room shell instead of the previous page. Parallelize the room preload and the auth token fetch in page.tsx to shorten the block before first paint. Generated-By: PostHog Code Task-Id: 10328b32-44d1-4998-bb84-dd4300bb0405
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
motz0815
left a comment
There was a problem hiding this comment.
Review: automated, review-only (no code changes)
What this PR actually changes
Three small changes to the host-room creation flow (+32/−8 across 3 files):
CreateRoomForm's RadixDialogbecomes controlled (open/onOpenChange), withsetOpen(false)called right beforerouter.push(/host/${code})inside the form action.- A new
app/host/[code]/loading.tsxrenders a spinner over the sameHostBackgroundused by the real host page. app/host/[code]/page.tsxrunspreloadQuery(getRoomByCode)andconvexAuthNextjsToken()in aPromise.allinstead of sequentially, keeping theisHostfetch after the room resolves.
Verdict: looks good
The diagnosis is accurate and the fix is real. No correctness bugs, races, or security issues in the diff. Two low-severity nits and one pre-existing gap worth a follow-up are below.
Verification of the PR's claims
- CONFIRMED — no loading boundary existed.
apps/web/src/appcontained zeroloading.tsxfiles before this PR (onlyroom/not-found.tsx). Arouter.pushinto/host/[code]had nothing to suspend into, so the transition stayed pending for the whole server round trip. - CONFIRMED — the spinner is transition-bound.
packages/ui/src/components/submit-button.tsxusesuseFormStatus().pending, which React clears only when the form-action transition commits. Sincerouter.pushand the action's pending-reset land on entangled transition lanes, the spinner and thebg-black/50DialogOverlay(packages/ui/src/components/dialog.tsx:40) really do survive the whole navigation. - CONFIRMED —
loading.tsxis the load-bearing fix. A newly mounted Suspense boundary lets the transition commit immediately with the fallback.setOpen(false)alone would not have fixed it — it's scheduled in the same transition and would be deferred identically. Worth stating explicitly so nobody later "simplifies" by deletingloading.tsxand keeping only the dialog change. setOpen(false)is redundant but not useless. Navigating away unmounts/host'smanage.tsxand with it the dialog, so the overlay would go regardless. But closing explicitly avoids Radix's known abrupt-unmount-while-open cleanup issue (stalepointer-events: noneon<body>/ scroll lock), so it's worth keeping. Making the dialog controlled is safe —Dialogatpackages/ui/src/components/dialog.tsx:9spreads props straight toDialogPrimitive.Root, and the twoCreateRoomForminstances inapps/web/src/app/host/manage.tsx:190,202are mutually exclusive branches with independent state.- CONFIRMED — the
Promise.allis safe.preloadQuery(api.rooms.getRoomByCode, …)is deliberately unauthenticated (the query inpackages/backend/convex/rooms.tstakes no identity), andconvexAuthNextjsToken()only reads the cookie set byapps/web/src/proxy.ts'sconvexAuthNextjsMiddleware. Ordering carries no auth semantics; both rejections are awaited, so there's no unhandled rejection. The only behavior change is that the cookie is now read even for nonexistent rooms — harmless, and the route was already dynamic.
Findings
1. apps/web/src/app/host/[code]/loading.tsx:11 — mobile viewport overflows by the parent's padding (cosmetic).
The outer div is min-h-screen … p-4 and the inner div is h-full min-h-screen … lg:min-h-0. Below the lg breakpoint the inner element's min-height: 100vh sits inside 1rem of parent padding, so the page is 100vh + 2rem tall and gets a ~32px scroll. The real page (apps/web/src/app/host/[code]/host.tsx:126) uses a plain flex h-full w-full main and does not do this, so the fallback→content transition will visibly jump on phones.
Fix: drop min-h-screen from the inner div and add min-h-[calc(100vh-2rem)], or just let the outer min-h-screen + flex do the centering.
2. apps/web/src/app/host/[code]/loading.tsx:14 — the fallback copy assumes the create-room path.
/host/[code] is also reached from the "Open" links on the manage page (apps/web/src/app/host/manage.tsx:178) and by anyone pasting a host URL. A non-host now sees "Setting up your room…" and then Next's default 404 (there is no host/not-found.tsx; only app/room/not-found.tsx exists). "Loading room…" would be accurate for all entry points.
3. apps/web/src/components/host/create-room.tsx:53-102 — no error handling on the action (pre-existing, but adjacent to the bug this PR fixes).
handleCreateRoom has no try/catch, and there is no error.tsx or global-error.tsx anywhere in apps/web/src/app.
Failure scenario: a first-time host (anonymous) submits; useAuthedMutation (apps/web/src/lib/auth.ts) calls signIn("anonymous") then waitForAuthentication(), which rejects with new Error("Authentication timeout") after 5s on a slow connection. The rejected form action propagates to the root error boundary and blanks the app instead of showing a toast in the still-open dialog.
Given the inbox report was specifically "first-time host, room unusable", this is a plausible second root cause of the same symptom that this PR does not cover. A .catch(err => { toast.error(...) }) around the mutation would close it.
4. PLAUSIBLE — the fix may convert "infinite spinner" into "404" for a freshly-signed-in anonymous host.
api.rooms.isHost (packages/backend/convex/rooms.ts:85-101) returns false when getAuthUserId is empty, and the page then calls notFound(). If the anonymous session's JWT cookie has not yet been written by the Convex auth middleware when the RSC render runs, the host lands on a 404 for a room they just created.
Not confirmed — @convex-dev/auth's client storage does appear to await the server cookie write inside signIn, so it's probably fine. Worth a manual check on the Vercel preview with a fresh incognito session, since it would look like the PR "didn't fix it".
CI
All 4 checks green: CodeQL (javascript-typescript and python), Vercel Preview Comments, and the Vercel songup preview deploy is Ready — so the build compiles. Note there is no lint or type-check workflow in .github/workflows; bun run lint / check-types exist in package.json but are not run in CI, so the Vercel build is the only real gate. The new file matches the repo's Prettier config (4-space, no semicolons, sorted imports, sorted Tailwind classes), and Loader2 is a valid lucide-react export already imported in packages/ui/src/components/button.tsx:3 and elsewhere.
Generated by Claude Code
Problem
CreateRoomFormpasseshandleCreateRoomstraight to<form action={...}>and callsrouter.pushfrom inside it. React keeps the form action pending for the whole route transition, soSubmitButtonkeeps spinning and the dialog never closes. Itsbg-black/50overlay stays over the old/hostpage.isHost), and there was no loading boundary anywhere inapps/web/src/app, so nothing covered the wait.Changes
components/host/create-room.tsxapp/host/[code]/loading.tsx(new)app/host/[code]/page.tsxNotes
open/onOpenChange), sorouter.pushcloses it before the route change starts.isHoststill runs after the room resolves, because it needs the room id — only the room preload and the token fetch are parallel.deploymentIdinnext.config.ts, no skew protection invercel.json). That is a separate issue and is not addressed here.Created with PostHog Desktop from this inbox report.