Skip to content

fix(app-router): resolve explicit parallel slot with no page (#1535)#1852

Merged
james-elicx merged 2 commits into
mainfrom
fix/issue-1535-explicit-slot-no-page
Jun 8, 2026
Merged

fix(app-router): resolve explicit parallel slot with no page (#1535)#1852
james-elicx merged 2 commits into
mainfrom
fix/issue-1535-explicit-slot-no-page

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Finishes the explicit-slot-but-no-page case of #1535 (children-slot priority landed in #1659).

Problem

When a parallel slot defines an explicit sub-page (e.g. @slot/baz/page.tsx) but there is no corresponding children page (baz/page.tsx) and no default.tsx for the children slot, vinext minted a synthetic /baz route with a null children page. That static route shadowed the sibling catch-all ([...catchAll]) and rendered an empty children prop, so the request hung — the upstream should match correctly when defining an explicit slot but no page test timed out.

Fix

Next.js serves such a slot-only sub-route's children from the nearest sibling catch-all. discoverSlotSubRoutes now falls the children prop through to a sibling catch-all page (findCatchAllPage) when no children default.tsx exists. For the upstream fixture, /baz renders [...catchAll]/page.tsx ("main catchall") for children while @slot/baz/page.tsx ("baz slot") fills the slot. A children default.tsx still wins over the catch-all when both are present.

The change is localized to discoverSlotSubRoutes; the children-slot priority case (/ and /nested) from #1659 is untouched and still passes.

Tests

Added a regression describe("explicit slot but no page (issue #1535)") in tests/app-route-graph.test.ts, ported from Next.js' test/e2e/app-dir/parallel-routes-catchall:

  • slot-only sub-route resolves children to the sibling catch-all and the slot to @slot/baz/page.tsx
  • a children default.tsx still wins over the catch-all when both exist

Closes #1535

When a parallel slot defines an explicit sub-page (e.g. `@slot/baz/page.tsx`)
but no corresponding children page (`baz/page.tsx`) and no `default.tsx` for
the children slot, vinext minted a synthetic `/baz` route with a null children
page. That route shadowed the sibling catch-all and rendered an empty children
prop, so the request hung (the upstream "explicit slot but no page" case timed
out).

Next.js serves such a slot-only sub-route's children from the nearest sibling
catch-all. Add `findCatchAllPage` and use it as the children fallback in
`discoverSlotSubRoutes` when no `default.tsx` exists, so `/baz` renders
`[...catchAll]/page.tsx` for children while `@slot/baz/page.tsx` fills the slot.
A children `default.tsx` still wins over the catch-all when both exist.

Finishes the explicit-slot-but-no-page case of #1535 (the children-slot
priority case landed in #1659).

Fixes #1535.
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1852
npm i https://pkg.pr.new/vinext@1852

commit: 1b127e8

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review: fix(app-router): resolve explicit parallel slot with no page (#1535)

Reviewed the change to discoverSlotSubRoutes and the new findCatchAllPage helper. The fix is well-scoped, the logic is correct for resolving the hang, and the [[...slug]]/[...slug] detection is sound. Tests pass and vp check is clean. One latent correctness gap worth flagging.

Verified

  • The slot-only sub-route (/baz) now materializes with the children prop falling through to the sibling [...catchAll]/page.tsx, so the static /baz no longer shadows the catch-all with an empty children prop. The top-level catch-all is preserved.
  • childrenDefault ?? childrenCatchAll correctly preserves default.tsx precedence (covered by the second test).
  • findCatchAllPage catch-all detection handles both required and optional catch-alls correctly: [[...slug]] matches the startsWith("[[...") && endsWith("]]") branch (it does not falsely match the [... branch), and regular [slug] dirs are excluded. The try/catch around readdirSync is appropriate.
  • Change lives entirely in the shared route-graph builder (app-route-graph.ts), so there is no dev/prod parity split to keep in sync.

Latent gap: catch-all children page receives empty params (not a blocker)

When children fall through to [...catchAll]/page.tsx, the synthetic /baz route is built with:

  • params: []
  • routeSegments: ["baz"]
  • isDynamic: false
  • patternParts: ["baz"] (static)

I confirmed this by inspecting the built route in a local debug run:

params: []   routeSegments: ["baz"]   isDynamic: false   pattern: /baz

At render time, the page component receives params verbatim from the matched route (app-page-element-builder.ts:197, pageProps = { params: makeThenableParams(params) }). Because the matched /baz route's pattern carries no :catchAll+ marker, the catch-all children page renders with empty paramsparams.catchAll is undefined, whereas Next.js would pass params.catchAll = ["baz"].

This is not exercised by the upstream fixture (its [...catchAll]/page.tsx just returns the string 'main catchall' and never reads params: https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/parallel-routes-catchall/app/%5B...catchAll%5D/page.tsx), and the PR's route-graph unit tests assert only pagePath equality, so neither catches it. The stated goal (resolve the hang, render the catch-all for children) is met.

But a real-world catch-all children page that reads params.catchAll would get undefined under vinext. Suggested follow-up:

  • Populate the catch-all param value for the children prop when the fallback is a catch-all page (derive catchAll from the matched sub-path segments), or
  • At minimum, add a test that reads params from the catch-all children page to lock in expected behavior and track the divergence.

Minor

  • The new describe is purely a route-graph test. Consider an App Router render/integration test (fixture under tests/fixtures/) for this scenario so the params behavior above is observable and protected against regression.

Overall: the fix is correct for the reported hang and safe to merge for its stated scope. The params gap should be tracked as a documented follow-up rather than left silent.

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Posted the review on PR #1852.

Summary

I reviewed the change to discoverSlotSubRoutes and the new findCatchAllPage helper. The fix is correct and well-scoped for its stated goal — resolving the hang where a slot-only sub-route shadowed a sibling catch-all with an empty children prop.

Verified good:

  • /baz now materializes with children falling through to [...catchAll]/page.tsx; the top-level catch-all is preserved.
  • childrenDefault ?? childrenCatchAll correctly preserves default.tsx precedence.
  • Catch-all detection handles both [...slug] and [[...slug]] correctly without false matches on plain [slug] dirs.
  • Change lives in the shared route-graph builder, so there's no dev/prod parity split.
  • Tests pass; vp check is clean.

One latent gap flagged (not a blocker):

  • The synthetic /baz route is built with params: [] / patternParts: ["baz"] (static), but its children pagePath is now the catch-all page. I confirmed via a debug run that the catch-all children page renders with empty paramsparams.catchAll is undefined, whereas Next.js passes ["baz"]. This isn't caught because the upstream fixture's catch-all page doesn't read params (just returns 'main catchall'), and the PR's unit tests only assert pagePath. I recommended a follow-up to populate the catch-all param for the children prop and an integration test to lock in the behavior.

The fix is safe to merge for its scope; the params divergence should be tracked as a documented follow-up rather than left silent.

github run

Address ask-bonk review: the synthetic slot-only sub-route (e.g. /baz) has a
static URL pattern, so a catch-all children page reading params.catchAll gets
empty params (Next.js passes ["baz"]). Document the divergence inline in
discoverSlotSubRoutes and add a graph-level test locking in the current static
shape so a future param-population fix must update it.
@james-elicx

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed the catch-all-children params gap in-PR:

  • Documented the divergence inline in discoverSlotSubRoutes (the synthetic sub-route's URL pattern is static, so a catch-all children page reading params.catchAll gets empty params where Next.js passes ["baz"]).
  • Added a graph-level test that locks in the current static route shape (isDynamic: false, params: [], patternParts: ["baz"]), so a future fix that populates the catch-all param must update that assertion rather than diverge silently.

The full param-population fix needs the slot-override-style request-time pattern matching threaded to the children prop (graph → manifest → wiring → element builder), which is broader than this hang fix; I've spun it off as a dedicated follow-up so this PR stays scoped to resolving the timeout.

@james-elicx james-elicx marked this pull request as ready for review June 8, 2026 15:28
@james-elicx james-elicx merged commit cfc0f81 into main Jun 8, 2026
51 checks passed
@james-elicx james-elicx deleted the fix/issue-1535-explicit-slot-no-page branch June 8, 2026 18:43
@github-actions github-actions Bot mentioned this pull request Jun 8, 2026
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.

Parallel routes: explicit @children slot/page should win over catchall and resolve when no default

1 participant