Problem
When navigating back from an intercepted route (e.g., /feed -> click photo -> browser back), the intercepted modal slot persists instead of being dismissed.
Root cause (updated)
The initial analysis was incomplete. The problem is NOT simply that absent slot keys survive the spread in mergeElements. There are two distinct scenarios where a slot key can be absent from the next response:
-
Soft nav to child route (e.g., /dashboard -> /dashboard/settings): The server only re-renders the matched route, not the parent layout's parallel slots. Absent slot keys should be PRESERVED from the previous state. This is the existing correct behavior.
-
Traversal from intercepted route (e.g., browser back from /feed with modal to /feed without): The server renders the destination route tree. The @modal slot is either absent or UNMATCHED_SLOT. The previous slot content (the intercepted modal) should be CLEARED, not preserved.
mergeElements handles both cases identically today: preserve previous slot content when the key is absent or has UNMATCHED_SLOT. This is correct for case 1 but wrong for case 2.
Why a simple mergeElements fix doesn't work
Adding "delete absent slot keys" to mergeElements breaks case 1 (the parallel slot content persists on soft navigation to child route E2E test). The function doesn't know whether it's being called for a soft nav or a traversal.
Fix direction
The fix needs navigation context. Options:
-
Thread a flag through the reducer: Add a clearUnmatchedSlots boolean to AppRouterAction. Set it to true for traversal actions. After mergeElements, the reducer deletes preserved slots that the server marked UNMATCHED_SLOT when the flag is set.
-
Post-process in the traversal path: Instead of changing mergeElements, have the traversal code path in app-browser-entry.ts strip stale slot entries from the elements before dispatching to the reducer.
-
Server-side signal: Have the server include explicit "clear this slot" markers (distinct from UNMATCHED_SLOT) for slots that should not be preserved on traversal.
Option 1 is probably the simplest correct approach: it keeps mergeElements generic and puts the navigation-aware logic where the navigation type is already known.
Reproduction
test("back then forward restores intercepted modal view", async ({ page }) => {
await page.goto("/feed");
await page.click("#feed-photo-42-link");
await expect(page.locator('[data-testid="photo-modal"]')).toBeVisible();
await page.goBack();
await expect(page.locator('[data-testid="feed-page"]')).toBeVisible();
await expect(page.locator('[data-testid="photo-modal"]')).not.toBeVisible(); // FAILS
});
Problem
When navigating back from an intercepted route (e.g.,
/feed-> click photo -> browser back), the intercepted modal slot persists instead of being dismissed.Root cause (updated)
The initial analysis was incomplete. The problem is NOT simply that absent slot keys survive the spread in
mergeElements. There are two distinct scenarios where a slot key can be absent from thenextresponse:Soft nav to child route (e.g.,
/dashboard->/dashboard/settings): The server only re-renders the matched route, not the parent layout's parallel slots. Absent slot keys should be PRESERVED from the previous state. This is the existing correct behavior.Traversal from intercepted route (e.g., browser back from
/feedwith modal to/feedwithout): The server renders the destination route tree. The@modalslot is either absent orUNMATCHED_SLOT. The previous slot content (the intercepted modal) should be CLEARED, not preserved.mergeElementshandles both cases identically today: preserve previous slot content when the key is absent or hasUNMATCHED_SLOT. This is correct for case 1 but wrong for case 2.Why a simple mergeElements fix doesn't work
Adding "delete absent slot keys" to
mergeElementsbreaks case 1 (theparallel slot content persists on soft navigation to child routeE2E test). The function doesn't know whether it's being called for a soft nav or a traversal.Fix direction
The fix needs navigation context. Options:
Thread a flag through the reducer: Add a
clearUnmatchedSlotsboolean toAppRouterAction. Set it totruefor traversal actions. AftermergeElements, the reducer deletes preserved slots that the server markedUNMATCHED_SLOTwhen the flag is set.Post-process in the traversal path: Instead of changing
mergeElements, have the traversal code path inapp-browser-entry.tsstrip stale slot entries from the elements before dispatching to the reducer.Server-side signal: Have the server include explicit "clear this slot" markers (distinct from
UNMATCHED_SLOT) for slots that should not be preserved on traversal.Option 1 is probably the simplest correct approach: it keeps
mergeElementsgeneric and puts the navigation-aware logic where the navigation type is already known.Reproduction