-
Notifications
You must be signed in to change notification settings - Fork 306
fix: clear stale parallel slots on traversal in mergeElements #816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
a7f6b93
b4c2b40
b26a410
6d23e51
d996df4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,11 @@ export const ParallelSlotsContext = React.createContext<Readonly< | |
| Record<string, React.ReactNode> | ||
| > | null>(null); | ||
|
|
||
| export function mergeElements(prev: AppElements, next: AppElements): AppElements { | ||
| export function mergeElements( | ||
| prev: AppElements, | ||
| next: AppElements, | ||
| clearAbsentSlots = false, | ||
| ): AppElements { | ||
| const merged: Record<string, AppElementValue> = { ...prev, ...next }; | ||
| // On soft navigation, unmatched parallel slots preserve their previous subtree | ||
| // instead of firing notFound(). Only hard navigation (full page load) should 404. | ||
|
|
@@ -32,6 +36,18 @@ export function mergeElements(prev: AppElements, next: AppElements): AppElements | |
| merged[key] = prev[key]; | ||
| } | ||
| } | ||
| // On traversal (browser back/forward), the server renders the full destination | ||
| // route tree. A slot absent from next means the destination route tree does not | ||
| // include it, so clear it rather than keeping the stale prev value. This only | ||
| // runs for traversals because soft forward navigations may omit parent layout | ||
| // slots that should be preserved. | ||
| if (clearAbsentSlots) { | ||
| for (const key of Object.keys(merged)) { | ||
| if (key.startsWith("slot:") && !Object.hasOwn(next, key)) { | ||
| delete merged[key]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (non-blocking): Iterating |
||
| } | ||
| } | ||
| } | ||
| return merged; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -277,6 +277,68 @@ describe("slot primitives", () => { | |
| expect(merged["slot:modal:/"]).toBe(UNMATCHED_SLOT); | ||
| }); | ||
|
|
||
| it("mergeElements clears stale slots absent from next when clearAbsentSlots is set", async () => { | ||
| const { mergeElements } = await import("../packages/vinext/src/shims/slot.js"); | ||
|
|
||
| const merged = mergeElements( | ||
| { | ||
| "layout:/": React.createElement("div", null, "layout"), | ||
| "page:/feed": React.createElement("div", null, "feed"), | ||
| "slot:modal:/feed": React.createElement("div", null, "intercepted modal"), | ||
| }, | ||
| { | ||
| "layout:/": React.createElement("div", null, "layout"), | ||
| "page:/feed": React.createElement("div", null, "feed"), | ||
| }, | ||
| true, | ||
| ); | ||
|
|
||
| expect(Object.hasOwn(merged, "slot:modal:/feed")).toBe(false); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a test for the interaction between This is the "traversal to a route that declares the slot but doesn't match it" case. The current code handles it correctly, but it's a subtle interaction worth pinning with a test. |
||
|
|
||
| it("mergeElements on traversal: UNMATCHED_SLOT in next is restored from prev and not cleared", async () => { | ||
| const { mergeElements, UNMATCHED_SLOT } = await import("../packages/vinext/src/shims/slot.js"); | ||
|
|
||
| const realContent = React.createElement("div", null, "modal content"); | ||
| const merged = mergeElements( | ||
| { | ||
| "layout:/": React.createElement("div", null, "layout"), | ||
| "page:/feed": React.createElement("div", null, "feed"), | ||
| "slot:modal:/feed": realContent, | ||
| }, | ||
| { | ||
| "layout:/": React.createElement("div", null, "layout"), | ||
| "page:/feed": React.createElement("div", null, "feed"), | ||
| "slot:modal:/feed": UNMATCHED_SLOT, | ||
| }, | ||
| true, | ||
| ); | ||
|
|
||
| // The slot IS present in next (as UNMATCHED_SLOT), so clearAbsentSlots does not | ||
| // delete it. The UNMATCHED_SLOT preservation loop then restores the real prev | ||
| // content because prev had a non-sentinel value. | ||
| expect(Object.hasOwn(merged, "slot:modal:/feed")).toBe(true); | ||
| expect(merged["slot:modal:/feed"]).toBe(realContent); | ||
| }); | ||
|
|
||
| it("mergeElements preserves absent slots when clearAbsentSlots is not set", async () => { | ||
| const { mergeElements } = await import("../packages/vinext/src/shims/slot.js"); | ||
|
|
||
| const merged = mergeElements( | ||
| { | ||
| "layout:/": React.createElement("div", null, "layout"), | ||
| "page:/dashboard": React.createElement("div", null, "dashboard"), | ||
| "slot:team:/dashboard": React.createElement("div", null, "team panel"), | ||
| }, | ||
| { | ||
| "page:/dashboard/settings": React.createElement("div", null, "settings"), | ||
| }, | ||
| ); | ||
|
|
||
| // Without clearAbsentSlots, absent slots survive (soft nav to child route) | ||
| expect(Object.hasOwn(merged, "slot:team:/dashboard")).toBe(true); | ||
| }); | ||
|
|
||
| it("Slot renders element from resolved context", async () => { | ||
| const mod = await import("../packages/vinext/src/shims/slot.js"); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call extracting this into a named helper with the doc comment. This directly addresses the prior review feedback about the duplicated ternary and makes the
"refresh"→"navigate"mapping explicit and centralized.