EasyStack

Browser Internals

The browser back button is one of the most familiar uses of a stack. Every web browser maintains a navigation stack that tracks your browsing history and makes back/forward work.

The Browser Navigation Stack

Every browser tab maintains a session history. This history is structured as a stack with a pointer to the current entry. Each entry represents a page visit and holds the URL, document state, and an optional state object.

When you navigate to a new URL, the browser pushes a new entry onto the history stack and moves the pointer forward. The stack grows as you visit more pages. When you close the tab, the entire history stack is destroyed.

The history stack is per-tab. Each tab has its own independent stack. Opening a link in a new tab creates a new stack.

How the Back Button Works

When you click the back button, the browser pops the current entry from the history stack and loads the previous entry. The current URL changes to the previous URL, and the page content updates accordingly.

Think of it as: the browser has a pointer called currentIndex. Initially it points to the top of the stack. When you go back, currentIndex decreases by one and the browser loads that entry. The popped entry is not destroyed immediately; it moves to a redo stack for the forward button.

How Forward Works

When you click the forward button, the browser takes the most recently popped entry and pushes it back onto the history stack, moving the pointer forward by one. This is the redo counterpart to the undo of the back button.

The forward button is only available when you have gone back at least once. If you visit a new page after going back, the forward entries are discarded (just like how undo history is cleared when you make a new change in a text editor).

pushState and replaceState

The History API lets you manipulate the browser stack from JavaScript without reloading the page. This is critical for single-page applications (SPAs).

history.pushState(state, title, url) adds a new entry to the history stack. The page does not reload. The URL bar updates to show the new URL. You can store a state object that the browser will pass back to you when the user navigates to this entry.

history.replaceState(state, title, url) replaces the current entry in the history stack instead of adding a new one. Use this when you want to update the URL without creating a new back-button entry.

// Add a new history entry
history.pushState({ page: 'settings' }, '', '/settings');

// Replace the current entry
history.replaceState({ page: 'home' }, '', '/');

popstate Event

The popstate event fires when the user navigates using the back or forward button, or when history.back(), history.forward(), or history.go() is called.

The event object has a state property containing the state object that was passed to pushState for that entry. If the entry was created by a normal page load (not pushState), the state is null.

window.addEventListener('popstate', function(event) {
    if (event.state) {
        renderPage(event.state.page);
    }
});

Note: popstate does not fire on the initial page load. Use the load event or check history.state on startup.

Session History vs Page State

The session history tracks which URLs you visited. The state object attached to each entry tracks application-specific data. They are separate concerns.

A common pattern is to store the current view, scroll position, or form data in the state object so that when the user returns via the back button, the application can restore its previous state. Without state objects, the browser only knows the URL, not what your app was displaying.

Building a Single-Page App Navigation System

To make a SPA with proper back-button support:

  1. Define routes as URL patterns (e.g., /home, /settings, /profile).
  2. When a route changes, call history.pushState with the new URL and route state.
  3. Listen for popstate to handle back/forward navigation.
  4. In the popstate handler, read the state and render the corresponding view.
  5. Handle the initial page load by checking location.pathname and rendering the right view.
function navigateTo(path, state) {
    history.pushState(state, '', path);
    renderView(state);
}

window.addEventListener('popstate', function(e) {
    renderView(e.state);
});

// Handle initial load
renderView({ page: location.pathname });

Common Pitfalls

Broken Back Button

The most common mistake in SPAs is not calling pushState on navigation. If the URL never changes, the back button takes the user out of the app entirely instead of to the previous view.

Not Handling popstate

If you listen for popstate but do not update the view, the URL changes but the content does not. Always pair pushState with a popstate handler.

Using hashchange Without pushState

Hash-based routing (#/page) works but produces ugly URLs. Modern SPAs should prefer pushState for clean URLs. Hash routing is a fallback for older browsers.

Forgetting Initial State

popstate does not fire on page load. You must handle the initial route separately by checking location.pathname and history.state when the app starts.

Frequently Asked Questions

What stack does the browser back button use?

The browser maintains a session history stack. Each time you visit a new page, it is pushed onto the stack. When you click back, the current entry is popped and the previous entry is loaded. The forward button uses a separate redo stack.

What is pushState?

pushState is a History API method that adds a new entry to the session history without reloading the page. It takes a state object, a title, and an optional URL. This is essential for single-page applications that need URL changes without full page reloads.

What is the popstate event?

The popstate event fires when the user clicks the back or forward button, or when history.back() or history.forward() is called. The event's state property contains the state object that was passed to pushState for that history entry.

How do I fix a broken back button in a single-page app?

Use the History API (pushState, replaceState, popstate) to manage navigation state. Each view transition should push a new history entry so the back button works naturally. Alternatively, use hash-based routing with hashchange events.