Welcome back! 👋 In the previous lesson, you mastered the window object. Now let's explore history — the browser object that gives JavaScript control over the user's navigation history!
The history object lets you move back and forward through the browser's session history, change the URL without reloading the page, and even manage navigation state — all essential skills for building modern single-page applications (SPAs).
You use it every time you click the browser's back button, navigate within a React or Vue app, or see the URL change without a full page reload. Let's master it completely!
What is the History Object?
The history object (window.history) represents the browser's session history — the list of pages visited in the current tab. It lets you navigate through that list and manipulate the URL programmatically.
console.log(window.history); // History object
console.log(history); // Same — window is implicit
console.log(history.length); // Number of entries in history stackBasic Navigation
history.back() — Go to Previous Page
Equivalent to clicking the browser's ← back button.
history.back();
// With a small delay (common pattern)
function goBack() {
history.back();
}
document.getElementById("backBtn").addEventListener("click", goBack);history.forward() — Go to Next Page
Equivalent to clicking the browser's → forward button.
history.forward();history.go() — Go to a Specific Position
Pass a positive or negative integer to jump through history.
history.go(-1); // Same as history.back()
history.go(1); // Same as history.forward()
history.go(-3); // Go back 3 pages
history.go(0); // Reload current page
history.go(); // Also reloads current pagehistory.length — How Many Entries Exist?
console.log(history.length);
// e.g. 5 — 5 pages visited in this tab's session
if (history.length > 1) {
console.log("Can go back ✅");
} else {
console.log("No history — this is the first page");
}Manipulating History — pushState and replaceState
These two methods are the foundation of Single Page Application (SPA) navigation. They let you change the URL in the browser's address bar without reloading the page.
history.pushState() — Add a New Entry
Pushes a new entry onto the history stack. The old URL is still accessible via the back button.
// Syntax: history.pushState(state, title, url)
history.pushState({ page: "home" }, "", "/home");
history.pushState({ page: "about" }, "", "/about");
history.pushState({ page: "contact" }, "", "/contact");
// URL in address bar changed — but no page reload!
console.log(window.location.pathname); // "/contact"
console.log(history.length); // Increased by 3history.replaceState() — Replace Current Entry
Replaces the current history entry — the old URL is not accessible via the back button.
// Syntax: history.replaceState(state, title, url)
history.replaceState({ page: "dashboard" }, "", "/dashboard");
// URL changed — but back button won't go to old URL
console.log(window.location.pathname); // "/dashboard"pushState vs replaceState
pushState | replaceState | |
|---|---|---|
| History stack | Adds new entry | Replaces current entry |
| Back button | Can go back to previous URL | Cannot go back to old URL |
| Use case | Normal navigation (new page) | Redirect, filter/sort changes |
// pushState — user CAN go back to /products
history.pushState({}, "", "/products/laptop");
// replaceState — user CANNOT go back to /products?sort=asc
history.replaceState({}, "", "/products?sort=desc");The state Object
The first argument to pushState and replaceState is a state object — arbitrary data you want to associate with that history entry. It's available when the user navigates back to that entry.
// Push with state data
history.pushState(
{ userId: 1, page: "profile", tab: "settings" },
"",
"/profile/settings"
);
// Access current state
console.log(history.state);
// { userId: 1, page: "profile", tab: "settings" }The popstate Event
When the user navigates back or forward (or when you call history.go()), the browser fires the popstate event on window. This is how SPAs know to update their content when navigation happens.
window.addEventListener("popstate", (event) => {
console.log("Navigation happened!");
console.log("State:", event.state);
console.log("New URL:", window.location.pathname);
});
// Now simulate navigation
history.pushState({ page: "home" }, "", "/home"); // no popstate fired
history.pushState({ page: "about" }, "", "/about"); // no popstate fired
history.pushState({ page: "contact" }, "", "/contact"); // no popstate fired
history.back(); // ← popstate fires! State: { page: "about" }
history.back(); // ← popstate fires! State: { page: "home" }⚠️ Important — pushState does NOT fire popstate
// pushState and replaceState do NOT trigger popstate
// Only user navigation (back/forward) or history.go() triggers it
window.addEventListener("popstate", (event) => {
// This runs when:
// ✅ User clicks browser back/forward button
// ✅ history.back() / history.forward() / history.go() is called
// ❌ history.pushState() — does NOT trigger this
// ❌ history.replaceState() — does NOT trigger this
});Building a Simple SPA Router
This is the core pattern used by React Router, Vue Router, and every SPA framework.
// Simple SPA router using pushState + popstate
const routes = {
"/": () => renderPage("Home", "Welcome to LearnJS! 🏠"),
"/courses": () => renderPage("Courses", "Browse all courses 📚"),
"/about": () => renderPage("About", "About us page ℹ️"),
"/contact": () => renderPage("Contact", "Get in touch 📞"),
};
function renderPage(title, content) {
document.title = `${title} — LearnJS`;
document.getElementById("app").innerHTML = `
<h1>${title}</h1>
<p>${content}</p>
`;
console.log(`Rendered: ${title}`);
}
function navigate(path) {
// Push new state — URL changes, no reload
history.pushState({ path }, "", path);
// Render the correct page
let route = routes[path] ?? routes["/"];
route();
}
// Handle browser back/forward
window.addEventListener("popstate", (event) => {
let path = event.state?.path ?? "/";
let route = routes[path] ?? routes["/"];
route();
});
// Handle clicks on nav links
document.querySelectorAll("[data-link]").forEach(link => {
link.addEventListener("click", (e) => {
e.preventDefault(); // Stop browser from reloading
navigate(link.getAttribute("href"));
});
});
// Render initial page on load
navigate(window.location.pathname);Real-World Examples
Example 1: Multi-Step Form with History
// A multi-step form where each step gets its own URL
// — so the back button goes to the previous step, not the previous site
let formSteps = {
1: { title: "Personal Info", fields: ["name", "email"] },
2: { title: "Contact Details", fields: ["phone", "address"] },
3: { title: "Review", fields: [] },
4: { title: "Confirmation", fields: [] },
};
let currentStep = 1;
function goToStep(step) {
if (step < 1 || step > 4) return;
let isForward = step > currentStep;
// Push new history entry for each step
history.pushState(
{ step },
"",
`/checkout/step-${step}`
);
currentStep = step;
renderStep(step);
}
function renderStep(step) {
let { title } = formSteps[step];
console.log(`📋 Step ${step}/4: ${title}`);
// In real app: show/hide form sections
}
// When user clicks browser back — go to previous step
window.addEventListener("popstate", (event) => {
let step = event.state?.step ?? 1;
currentStep = step;
renderStep(step);
});
// Simulate navigation through steps
goToStep(1); // /checkout/step-1
goToStep(2); // /checkout/step-2
goToStep(3); // /checkout/step-3
history.back(); // Goes back to step 2 via popstateExample 2: URL-Based Tab System
// Tab system that updates the URL — so tabs are shareable and bookmarkable
const tabs = ["overview", "curriculum", "reviews", "faq"];
function switchTab(tabName) {
if (!tabs.includes(tabName)) return;
// Update URL with replaceState — tabs don't add to back history
history.replaceState(
{ tab: tabName },
"",
`/course/javascript?tab=${tabName}`
);
// Update UI
tabs.forEach(t => {
let tabEl = document.getElementById(`tab-${t}`);
let contentEl = document.getElementById(`content-${t}`);
if (tabEl) tabEl.classList.toggle("active", t === tabName);
if (contentEl) contentEl.style.display = t === tabName ? "block" : "none";
});
console.log(`Switched to tab: ${tabName}`);
}
// Read tab from URL on page load
function initTabsFromURL() {
let params = new URLSearchParams(window.location.search);
let tab = params.get("tab") ?? "overview";
switchTab(tab);
}
initTabsFromURL();Example 3: Search with URL Sync
// Search results that sync to URL — shareable search links
function updateSearchURL(query, filters = {}) {
let params = new URLSearchParams();
if (query) params.set("q", query);
if (filters.category) params.set("category", filters.category);
if (filters.sort) params.set("sort", filters.sort);
if (filters.page) params.set("page", filters.page);
let newURL = `/search${params.toString() ? "?" + params.toString() : ""}`;
history.pushState(
{ query, filters },
"",
newURL
);
console.log(`Search URL: ${newURL}`);
}
function readSearchFromURL() {
let params = new URLSearchParams(window.location.search);
return {
query: params.get("q") ?? "",
category: params.get("category") ?? "all",
sort: params.get("sort") ?? "relevance",
page: parseInt(params.get("page") ?? "1", 10),
};
}
// When user goes back/forward — restore search state
window.addEventListener("popstate", (event) => {
let { query, filters } = event.state ?? readSearchFromURL();
console.log("Restored search:", { query, filters });
// Re-run search with restored params
});
// Simulate user searching
updateSearchURL("JavaScript", { category: "courses", sort: "popular", page: 1 });
updateSearchURL("React", { category: "courses", sort: "newest", page: 1 });Example 4: Breadcrumb Navigation
// Breadcrumb trail using history state
let breadcrumbs = [];
function navigateTo(path, label) {
breadcrumbs.push({ path, label });
history.pushState(
{ breadcrumbs: [...breadcrumbs] },
"",
path
);
renderBreadcrumbs();
}
function renderBreadcrumbs() {
let trail = breadcrumbs.map((crumb, i) => {
let isLast = i === breadcrumbs.length - 1;
return isLast
? `[${crumb.label}]`
: `${crumb.label} >`;
}).join(" ");
console.log(`🧭 ${trail}`);
}
// Restore breadcrumbs on back/forward
window.addEventListener("popstate", (event) => {
breadcrumbs = event.state?.breadcrumbs ?? [];
renderBreadcrumbs();
});
// Simulate navigation
navigateTo("/", "Home");
navigateTo("/courses", "Courses");
navigateTo("/courses/js", "JavaScript");
navigateTo("/courses/js/dom", "DOM & BOM");
history.back(); // Goes back to /courses/js
history.back(); // Goes back to /courses
// Output:
// 🧭 [Home]
// 🧭 Home > [Courses]
// 🧭 Home > Courses > [JavaScript]
// 🧭 Home > Courses > JavaScript > [DOM & BOM]
// 🧭 Home > Courses > [JavaScript]
// 🧭 Home > [Courses]Common Mistakes
Mistake 1: Expecting pushState to Fire popstate
// ❌ pushState does NOT trigger popstate — ever!
window.addEventListener("popstate", () => {
console.log("This won't run on pushState");
});
history.pushState({}, "", "/new-page"); // popstate NOT fired ❌
// ✅ After pushState, manually update UI yourself
history.pushState({ page: "about" }, "", "/about");
renderAboutPage(); // You must call render manually ✅Mistake 2: Using pushState for Filters — Use replaceState
// ❌ Using pushState for sort/filter — back button becomes cluttered!
document.getElementById("sortSelect").addEventListener("change", (e) => {
history.pushState({}, "", `?sort=${e.target.value}`); // ❌ adds to history
});
// ✅ Use replaceState for filters — doesn't pollute history
document.getElementById("sortSelect").addEventListener("change", (e) => {
history.replaceState({}, "", `?sort=${e.target.value}`); // ✅ replaces current
});Mistake 3: Not Handling the Initial Page Load
// ❌ Only handling popstate — but what about the initial URL?
window.addEventListener("popstate", (event) => {
renderPage(event.state?.page);
});
// On first load, popstate never fires — page is blank! ❌
// ✅ Handle initial load separately
window.addEventListener("popstate", (event) => {
renderPage(event.state?.page ?? "/");
});
// Always render on first load too
renderPage(window.location.pathname); // ✅Mistake 4: Forgetting e.preventDefault() on Link Clicks
// ❌ Without preventDefault — browser reloads on link click!
document.querySelectorAll("a[data-link]").forEach(link => {
link.addEventListener("click", (e) => {
// Missing e.preventDefault() — browser navigates normally ❌
navigate(link.getAttribute("href"));
});
});
// ✅ Always prevent default for SPA navigation
document.querySelectorAll("a[data-link]").forEach(link => {
link.addEventListener("click", (e) => {
e.preventDefault(); // Stop full reload ✅
navigate(link.getAttribute("href"));
});
});History Object Cheat Sheet
| Method / Property | What it does |
|---|---|
history.length | Number of entries in session history |
history.state | State object of current history entry |
history.back() | Go to previous page (like ← button) |
history.forward() | Go to next page (like → button) |
history.go(n) | Jump n steps (negative = back, positive = forward) |
history.go(0) | Reload current page |
history.pushState(state, title, url) | Add new history entry, change URL |
history.replaceState(state, title, url) | Replace current entry, change URL |
popstate event | Fires on back/forward navigation |
event.state | State object passed to push/replaceState |
Key Takeaways
Congratulations! 🎉 You now fully understand the history object and how to control browser navigation with JavaScript.
✅ history.back(), history.forward(), history.go(n) — navigate through session history programmatically.
✅ pushState(state, title, url) — adds a new entry to history, changes URL, no page reload. Use for normal page navigation.
✅ replaceState(state, title, url) — replaces current entry, changes URL, no page reload. Use for filters, sorting, and redirects.
✅ state object — attach data to a history entry. Access it via history.state or event.state in popstate.
✅ popstate event — fires when user navigates back/forward. Use it to update UI without a page reload.
✅ pushState does NOT fire popstate — you must update UI manually after calling it.
✅ Always handle initial page load — popstate doesn't fire on first visit. Read from window.location instead.
Best Practices
- ✅ Use
pushStatefor navigating to new "pages" in SPAs - ✅ Use
replaceStatefor filter/sort changes, redirects, and tab switching - ✅ Always
e.preventDefault()on link clicks to prevent full page reloads - ✅ Always handle initial page load separately —
popstatedoesn't fire on first visit - ✅ Store meaningful state objects in
pushState— you'll need them inpopstate - ✅ Use
URLSearchParamswithpushStatefor query string management - ✅ Keep state objects serializable — no functions, Dates, or undefined values
- ✅ Don't overuse
pushState— only add history entries for meaningful navigation steps
What's Next?
Great work! 🎉 You now know how to control browser navigation like a pro.
Next up, let's look at what the browser knows about itself and the device:
Navigator Object — browser and device information including user agent, language, online status, geolocation, and platform!
Let's keep going! 💪