Handling Browser History: Building SPA Navigation in React
Learn how to manage browser history for a smooth, single-page application feel. Master the History API to navigate React apps without full page refreshes.
Previously in this course, we focused on polishing the UI and finalizing the movie browser. While our app looks great, it currently functions as a single "page." In this lesson, we'll evolve it into a true SPA (Single-Page Application) by learning how to control the browser's URL and history stack without triggering a full page refresh.
The Problem: Why Browser History Matters
In a traditional multi-page website, clicking a link fetches a new HTML document from the server. This is slow and destroys your application's state. In an SPA, we want the user to feel like they are moving between different pages, but the browser should only render different components while keeping the JavaScript context alive.
To achieve this, we need to manipulate the window.history object. This browser API allows us to programmatically change the URL displayed in the address bar and manage the "Back" and "Forward" buttons, all without a network request.
Core Concept: The History API
The browser provides the History API, which gives us two primary methods for navigation:
pushState: Adds a new entry to the browser's history stack. This is what happens when you click a link to a new page.replaceState: Modifies the current history entry. Useful for things like redirecting or updating query parameters without adding a new item to the "Back" stack.
Worked Example: Building a Manual Router
Let's implement a basic navigation handler for our movie browser. We want to navigate to a "Movie Detail" view when a user clicks a movie card.
First, we create a function to handle the URL update:
JAVASCRIPTconst navigateTo = (path) => { // Update the browser URL without a page reload window.history.pushState({}, "", path); // Create a custom event to notify our app that the URL changed const navEvent = new PopStateEvent("popstate"); window.dispatchEvent(navEvent); };
Now, we need to listen for when the user clicks the browser's back button. This triggers a popstate event:
JAVASCRIPTimport { useState, useEffect } from "react"; function AppRouter() { const [currentPath, setCurrentPath] = useState(window.location.pathname); useEffect(() => { const onLocationChange = () => { setCurrentPath(window.location.pathname); }; window.addEventListener("popstate", onLocationChange); // Cleanup the event listener return () => { window.removeEventListener("popstate", onLocationChange); }; }, []); // Simple routing logic return ( <div> {currentPath === "/" && <MovieList />} {currentPath.startsWith("/movie/") && <MovieDetails />} </div> ); }
Hands-on Exercise
In your movie browser project, create a Link component that replaces the standard <a> tag.
- Create a component that accepts a
toprop. - Inside, render an
<a>tag, but add anonClickhandler. - In the handler, prevent the default browser behavior with
e.preventDefault(). - Call the
window.history.pushStatemethod to update the URL. - Trigger a custom event so your
AppRouterknows to update thecurrentPathstate.
Common Pitfalls
- Forgetting
preventDefault(): If you use standard<a>tags without callinge.preventDefault(), the browser will attempt to navigate to a real server URL, causing a full page refresh and losing your app state. - State Desync: If you update the URL but don't update your React state (like
currentPath), the UI won't re-render to show the new content. Always ensure your "router" state is the single source of truth. - The "Back" Button Black Hole: If you update the URL using
pushStatebut don't listen to thepopstateevent, the browser's back button will change the URL in the address bar, but your app will stay stuck on the same component.
Recap
Managing browser history is the secret sauce behind the SPA experience. By using the History API, we take control of the URL, allowing for bookmarkable links and functional navigation buttons while maintaining the performance benefits of a single-page application. Understanding these routing fundamentals from scratch is critical before moving on to professional libraries like React Router.
Up next: We will tackle data persistence by learning how to use LocalStorage to save user preferences across sessions.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds โ familiar editing, modern speed.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js โ charts, tables, and real-time data that your users will actually enjoy using.