[PR #747] [MERGED] feat: add client primitives for layout persistence (Slot, Children, mergeElementsPromise) #814

Closed
opened 2026-05-06 13:10:14 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/747
Author: @NathanDrake2406
Created: 4/2/2026
Status: Merged
Merged: 4/2/2026
Merged by: @james-elicx

Base: mainHead: feat/layout-persistence-pr-2a


📝 Commits (3)

  • 5d8525b Add slot client primitives
  • aed3514 fix: address review feedback on slot primitives
  • 6a2aa52 fix: prototype pollution in Slot, freeze shared default, improve tests

📊 Changes

2 files changed (+351 additions, -0 deletions)

View changed files

packages/vinext/src/shims/slot.tsx (+85 -0)
tests/slot.test.ts (+266 -0)

📄 Description

Summary

Part of #726 (PR 2a). Adds the client-side composition primitives that the flat keyed map architecture needs. These are purely additive — no existing code consumes them yet. PR 2c ("The Switch") wires them into the browser/SSR entries.

New file: packages/vinext/src/shims/slot.tsx

Four "use client" exports that implement the Waku-inspired slot/children pattern:

  • Slot — The core composition primitive. Reads an entry from the shared Promise<Elements> map by ID, provides children and parallel slots via context. Handles three states:
    • Key absent → return null (entry persists from prior soft nav)
    • UNMATCHED_SLOT sentinel → call notFound() (no default.js exists)
    • Any other value (including null) → render normally
  • Children — How layouts render their child content. Reads from ChildrenContext, which Slot sets when rendering a layout entry. This maintains Next.js's {children} prop API.
  • ParallelSlot — How layouts render named parallel slots (@modal, @sidebar). Reads from ParallelSlotsContext.
  • mergeElementsPromise — The layout persistence mechanism. Shallow-merges new entries into existing ones with a double-nested WeakMap cache for referential stability (same (prev, next) promise pair always returns the same output Promise — critical for avoiding unnecessary React re-renders via use()).

Design decisions

UNMATCHED_SLOT uses Symbol.for("vinext.unmatchedSlot") instead of null — because default.tsx can legitimately return null (e.g., feed/@modal/default.tsx). Using null as the sentinel would make it impossible to distinguish "render default.js which returned null" from "no default.js exists." See design discussion in #726.

RSC wire format note: Symbols can't survive React's flight serialization. PR 2c handles the translation — the server uses a serializable marker, and the client translates to the Symbol after createFromFetch(). The Slot component always checks against the Symbol, never the wire format.

Contexts

Context Default Set by Read by
ElementsContext Empty resolved promise BrowserRoot (PR 2c) Slot
ChildrenContext null Slot Children
ParallelSlotsContext null Slot (when parallelSlots prop provided) ParallelSlot

Test plan

New test file: tests/slot.test.ts (10 tests)

  • All exports exist with correct types
  • Children renders null outside a Slot provider
  • ParallelSlot renders null outside a Slot provider
  • Slot renders matched element and provides children + parallel slots via context
  • Slot returns null for absent entries (soft nav persistence)
  • Slot throws notFound() for UNMATCHED_SLOT sentinel (with correct digest)
  • Slot renders null entries without triggering notFound (default.tsx returning null)
  • mergeElementsPromise shallow-merges with correct key semantics
  • mergeElementsPromise caches by input promise identity (WeakMap memoization)
  • Slot suspends on pending elements promise and streams with Suspense fallback
vp test run tests/slot.test.ts

Refs #726


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/747 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/2/2026 **Status:** ✅ Merged **Merged:** 4/2/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `feat/layout-persistence-pr-2a` --- ### 📝 Commits (3) - [`5d8525b`](https://github.com/cloudflare/vinext/commit/5d8525b9f72a9f5df0054cf2b46bfaf0c08c9a97) Add slot client primitives - [`aed3514`](https://github.com/cloudflare/vinext/commit/aed351421b8fbab3b692d13a50d5617ea6825b0b) fix: address review feedback on slot primitives - [`6a2aa52`](https://github.com/cloudflare/vinext/commit/6a2aa52d46605439af4a79676a20db4ed49c4ff1) fix: prototype pollution in Slot, freeze shared default, improve tests ### 📊 Changes **2 files changed** (+351 additions, -0 deletions) <details> <summary>View changed files</summary> ➕ `packages/vinext/src/shims/slot.tsx` (+85 -0) ➕ `tests/slot.test.ts` (+266 -0) </details> ### 📄 Description ## Summary Part of #726 (PR 2a). Adds the client-side composition primitives that the flat keyed map architecture needs. These are purely additive — no existing code consumes them yet. PR 2c ("The Switch") wires them into the browser/SSR entries. ### New file: `packages/vinext/src/shims/slot.tsx` Four `"use client"` exports that implement the Waku-inspired slot/children pattern: - **`Slot`** — The core composition primitive. Reads an entry from the shared `Promise<Elements>` map by ID, provides children and parallel slots via context. Handles three states: - Key absent → return null (entry persists from prior soft nav) - `UNMATCHED_SLOT` sentinel → call `notFound()` (no `default.js` exists) - Any other value (including `null`) → render normally - **`Children`** — How layouts render their child content. Reads from `ChildrenContext`, which `Slot` sets when rendering a layout entry. This maintains Next.js's `{children}` prop API. - **`ParallelSlot`** — How layouts render named parallel slots (`@modal`, `@sidebar`). Reads from `ParallelSlotsContext`. - **`mergeElementsPromise`** — The layout persistence mechanism. Shallow-merges new entries into existing ones with a double-nested WeakMap cache for referential stability (same `(prev, next)` promise pair always returns the same output Promise — critical for avoiding unnecessary React re-renders via `use()`). ### Design decisions **`UNMATCHED_SLOT` uses `Symbol.for("vinext.unmatchedSlot")` instead of `null`** — because `default.tsx` can legitimately return `null` (e.g., `feed/@modal/default.tsx`). Using `null` as the sentinel would make it impossible to distinguish "render default.js which returned null" from "no default.js exists." See [design discussion in #726](https://github.com/cloudflare/vinext/issues/726#issuecomment-4167423501). **RSC wire format note:** Symbols can't survive React's flight serialization. PR 2c handles the translation — the server uses a serializable marker, and the client translates to the Symbol after `createFromFetch()`. The `Slot` component always checks against the Symbol, never the wire format. ### Contexts | Context | Default | Set by | Read by | |---------|---------|--------|---------| | `ElementsContext` | Empty resolved promise | `BrowserRoot` (PR 2c) | `Slot` | | `ChildrenContext` | `null` | `Slot` | `Children` | | `ParallelSlotsContext` | `null` | `Slot` (when `parallelSlots` prop provided) | `ParallelSlot` | ## Test plan New test file: `tests/slot.test.ts` (10 tests) - [x] All exports exist with correct types - [x] `Children` renders null outside a `Slot` provider - [x] `ParallelSlot` renders null outside a `Slot` provider - [x] `Slot` renders matched element and provides children + parallel slots via context - [x] `Slot` returns null for absent entries (soft nav persistence) - [x] `Slot` throws `notFound()` for `UNMATCHED_SLOT` sentinel (with correct digest) - [x] `Slot` renders `null` entries without triggering `notFound` (default.tsx returning null) - [x] `mergeElementsPromise` shallow-merges with correct key semantics - [x] `mergeElementsPromise` caches by input promise identity (WeakMap memoization) - [x] `Slot` suspends on pending elements promise and streams with Suspense fallback ``` vp test run tests/slot.test.ts ``` Refs #726 --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:10:14 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#814
No description provided.