Skip to content

fix(router): push history entry when navigating between sibling views - #321

Closed
techninja wants to merge 2 commits into
hybridsjs:mainfrom
techninja:fix/router-sibling-navigation-pushstate
Closed

techninja wants to merge 2 commits into
hybridsjs:mainfrom
techninja:fix/router-sibling-navigation-pushstate

Conversation

@techninja

Copy link
Copy Markdown

Hey! Long time user, first time patcher? Hope this is right and I'm not just setting my views up wrong!

Problem

Navigating between sibling views (views that share a parent stack but are not in a parent/child relationship) always replaces the current history entry instead of pushing a new one. The browser back button skips over all sibling navigation.

Root cause

In getEntryOffset(), when the target view is not found anywhere in the current history state, the loop falls through to:

if (j === state[i].length) {
  offset = state[i].length - 1;
}

For a single-entry state this sets offset = 0. navigate() then calls navigateBack(0, ...), which always calls replaceState when state.length === 1.

Fix

Return -1 from the fallthrough case. This signals to navigate() that the target is not in history, so it takes the pushState branch.

Test

Added a standalone test: two URL siblings in a shared stack — navigating A→B pushes a new history entry and history.back() returns to A.

Passing locally:

Chrome Headless 154.0.0.0 (Mac OS 10.15.7): Executed 617 of 617 SUCCESS (48.904 secs / 47.839 secs)
................................................................................
................................................................................
................................................................................
...........................
WebKit 605.1.15 (Mac OS 10.15.7): Executed 617 of 617 SUCCESS (48.656 secs / 47.31 secs)
TOTAL: 1234 SUCCESS

smalluban and others added 2 commits September 30, 2026 15:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getEntryOffset() fell through to set offset = state[i].length - 1
when the target view was not found in the current history state.
For a single-entry state this returned 0, causing navigate() to call
navigateBack(0) which always calls replaceState instead of pushState.

Returning -1 from the fallthrough case correctly signals that the
target is not in history, so navigate() takes the pushState branch.

Adds a test: two URL siblings in a shared stack — navigating A→B
pushes a new history entry and back() returns to A.
@smalluban

Copy link
Copy Markdown
Contributor

Thanks for digging into this and for the clear write-up! But this is intended behavior. Views that aren't in a parent/child relationship replace the history entry. Only views listed in the current view's stack are pushed. The offset = state[i].length - 1 fallback handles exactly the case where the target isn't a descendant of the current path, so the router resets history to the target (docs: "navigating between roots always replaces the whole stack", and the LCA explanation in View → stack).

You can see it in the existing "uses push navigation to view with multiple parents" test, which this PR had to change from 2 to 3. Returning -1 there would also push across different roots and nested sibling stacks, so history would grow without limit.

If you want B to be pushed when you navigate from A, add it to A's stack: [router.connect]: { stack: [B] }. I'm closing this one, but feel free to open a discussion if your structure doesn't fit that model. Happy to help.

@smalluban smalluban closed this Oct 5, 2026
@techninja

Copy link
Copy Markdown
Author

Thanks so much for the quick reply, keep up the great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants