The Problem
In Single Page Applications (SPAs), users navigate between views without full page reloads. When they press the browser back button, the URL changes but the application state may be lost, showing wrong data or a blank page.
Symptoms
- Back button changes URL but view does not update
- Form inputs are cleared when navigating back
- Scroll position resets to top instead of previous position
- Search filters or pagination state is lost
- User sees a different page than expected
Common Causes
Cause 1: Not Using History API
```javascript // WRONG: Changing URL without history state window.location.hash = '#/search?q=react';
// On back button, the hash changes but the app does not know ```
Cause 2: Not Listening to popstate
```javascript // WRONG: No popstate listener history.pushState({ page: 'search' }, '', '/search?q=react');
// Back button fires popstate, but nothing handles it ```
How to Fix It
Fix 1: Use History API with State
javascript
// When navigating
function navigateToSearch(query) {
const state = { query, page: 'search', timestamp: Date.now() };
history.pushState(state, '', /search?q=${encodeURIComponent(query)}`);
performSearch(query);
}
// Handle back/forward window.addEventListener('popstate', (event) => { const state = event.state; if (state && state.page === 'search') { performSearch(state.query); } else { // Default page showHome(); } });
// Handle initial load window.addEventListener('load', () => { if (history.state) { // Restore from state handleRoute(history.state); } else { // Parse URL handleRoute(parseUrl(window.location)); } }); ```
Fix 2: Save and Restore Scroll Position
```javascript const scrollPositions = new Map();
window.addEventListener('popstate', () => { const savedPos = scrollPositions.get(window.location.pathname) || 0; window.scrollTo(0, savedPos); });
// Save scroll position before navigation function saveScrollPosition() { scrollPositions.set(window.location.pathname, window.scrollY); }
// Call before any navigation document.querySelectorAll('a').forEach(link => { link.addEventListener('click', saveScrollPosition); }); ```
Fix 3: Use a Router Library
```javascript // React Router handles all of this automatically import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
function App() { return ( <BrowserRouter> <Routes> <Route path="/search" element={<SearchPage />} /> <Route path="/" element={<HomePage />} /> </Routes> </BrowserRouter> ); } ```
// Vue Router
const router = createRouter({
history: createWebHistory(),
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0 };
}
});Fix 4: Serialize Complex State to URL
```javascript // Encode filters in URL so back button restores them function updateFilters(filters) { const params = new URLSearchParams(window.location.search); Object.entries(filters).forEach(([key, value]) => { if (value) params.set(key, value); else params.delete(key); });
const url = ?${params.toString()};
history.pushState({ filters }, '', url);
}
```
Fix 5: Handle State in sessionStorage
```javascript // For complex state that cannot fit in URL function saveAppState(state) { sessionStorage.setItem('app-state', JSON.stringify(state)); }
function restoreAppState() { const saved = sessionStorage.getItem('app-state'); return saved ? JSON.parse(saved) : null; }
window.addEventListener('popstate', () => { const state = restoreAppState(); if (state) restoreUI(state); }); ```