Location & History
The URL is the most important piece of state in any web page: it identifies what the user is looking at, it can be bookmarked, and it survives a refresh. JavaScript exposes two browser objects for working with it — window.location reads and changes the current URL, and window.history lets you move through the session’s navigation stack and update the URL without a full page reload. Together they power everything from a simple redirect to a full single-page application (SPA) router.
Reading the URL with window.location
window.location (usually just location) is a Location object that breaks the current URL into addressable parts. Reading any property is synchronous and side-effect free, so it is safe to inspect at any time.
// Suppose the page is https://shop.example.com:8443/products?id=42&sort=price#reviews
console.log(location.href); // full URL
console.log(location.protocol); // "https:"
console.log(location.host); // "shop.example.com:8443"
console.log(location.hostname); // "shop.example.com"
console.log(location.port); // "8443"
console.log(location.pathname); // "/products"
console.log(location.search); // "?id=42&sort=price"
console.log(location.hash); // "#reviews"
console.log(location.origin); // "https://shop.example.com:8443"
Output:
https://shop.example.com:8443/products?id=42&sort=price#reviews
https:
shop.example.com:8443
shop.example.com
8443
/products
?id=42&sort=price
#reviews
https://shop.example.com:8443
| Property | Meaning | Writable |
|---|---|---|
href | The complete URL | Yes (navigates) |
protocol | Scheme including :, e.g. https: | Yes |
host / hostname | Domain with / without port | Yes |
pathname | Path after the host | Yes |
search | Query string including ? | Yes |
hash | Fragment including # | Yes |
origin | protocol + host | No (read-only) |
Changing the location
Assigning to location.href (or calling location.assign()) navigates the browser to a new page and adds an entry to the history stack, so the back button returns to the current page. location.replace() navigates too, but overwrites the current history entry, so back skips it — ideal after a login redirect. location.reload() re-requests the current page.
location.href = "/dashboard"; // navigate, keeps history
location.assign("/dashboard"); // identical to the line above
location.replace("/login"); // navigate, no back-button entry
location.reload(); // reload current page from cache/network
Setting just one part of the URL re-navigates as well. Changing location.hash is special: it scrolls to the fragment and fires a hashchange event without reloading the document.
Tip: prefer
location.assign()/location.replace()over string assignment in code reviews — the intent (and whether a history entry is created) is explicit.
Parsing query strings with URLSearchParams
Manually splitting location.search is error-prone because of URL encoding and repeated keys. URLSearchParams parses, reads, and serializes query strings correctly, including %20-style encoding.
const params = new URLSearchParams(location.search);
params.get("id"); // "42" (null if missing)
params.has("sort"); // true
params.getAll("tag"); // ["a", "b"] for ?tag=a&tag=b
params.set("page", "2"); // add or overwrite
params.delete("sort");
// Serialize back to a query string (auto-encoded)
console.log(params.toString());
Output:
id=42&page=2
For a full URL you can use the URL constructor, which gives you a live searchParams object:
const url = new URL("https://api.example.com/search?q=hello world");
url.searchParams.set("limit", "10");
console.log(url.href); // "https://api.example.com/search?q=hello+world&limit=10"
Both URL and URLSearchParams work identically in the browser and in Node.js, making them safe to share between client and server code.
The History API for SPAs
history.pushState() and history.replaceState() change the URL shown in the address bar and the entry in the back/forward stack without triggering a navigation or page load. This is the foundation of client-side routers: you swap the DOM yourself and update the URL so it stays bookmarkable.
// pushState(state, unused, url)
history.pushState({ view: "profile" }, "", "/users/jane");
history.replaceState({ view: "home" }, "", "/");
history.back(); // like the back button
history.forward(); // like the forward button
history.go(-2); // jump two entries back
console.log(history.length); // number of entries in this session
The first argument is a structured-clone-able state object stored with the entry. When the user presses back or forward, the browser fires a popstate event carrying that state — that is your cue to re-render. Note that pushState/replaceState do not fire popstate themselves.
Warning: the URL you pass to
pushStatemust be same-origin, or the browser throws aSecurityError. You cannot use it to fake another site’s address.
Here is a minimal, self-contained SPA router you can run live:
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui; padding: 1.5rem; }
nav a { margin-right: 1rem; cursor: pointer; color: #2563eb; }
#view { margin-top: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
</style>
</head>
<body>
<nav>
<a data-route="/">Home</a>
<a data-route="/about">About</a>
<a data-route="/contact">Contact</a>
</nav>
<div id="view"></div>
<script>
const views = {
"/": "Welcome home. Use back/forward — the URL stays in sync.",
"/about": "About us: built with the History API, no page reloads.",
"/contact": "Contact: email [email protected]",
};
const render = (path) => {
document.getElementById("view").textContent =
views[path] ?? `No view for ${path}`;
};
// Intercept link clicks and push a new history entry
document.querySelector("nav").addEventListener("click", (e) => {
const link = e.target.closest("[data-route]");
if (!link) return;
const path = link.dataset.route;
history.pushState({ path }, "", path);
render(path);
});
// Re-render when the user navigates back/forward
window.addEventListener("popstate", (e) => {
render(e.state?.path ?? location.pathname);
});
render(location.pathname === "/" ? "/" : location.pathname);
</script>
</body>
</html>
Reacting to URL changes
For hash-based routing, listen for hashchange; for the History API, listen for popstate. This demo shows both fragment navigation and reading the parsed query string at once:
<!DOCTYPE html>
<html>
<head><style>body{font-family:system-ui;padding:1.5rem}button{margin-right:.5rem}</style></head>
<body>
<button id="go">Set ?count=… in URL</button>
<a href="#section-2">Jump to #section-2</a>
<p id="out"></p>
<script>
const out = document.getElementById("out");
let count = 0;
document.getElementById("go").addEventListener("click", () => {
const url = new URL(location.href);
url.searchParams.set("count", String(++count));
history.replaceState(null, "", url); // update URL, no new entry
out.textContent = `Query is now: ${url.search}`;
});
window.addEventListener("hashchange", () => {
out.textContent = `Hash changed to: ${location.hash}`;
});
</script>
</body>
</html>
Best practices
- Use
URLSearchParams/URLinstead of hand-parsinglocation.search— they handle encoding and duplicate keys correctly. - Choose
replace()/replaceState()(no new history entry) for redirects and transient state; useassign()/pushState()when the user should be able to go back. - Always store the data you need to re-render in the
pushStatestate object, and read it back in yourpopstatehandler. - Keep
pushStateURLs same-origin and meaningful — they will be bookmarked and shared. - Treat the URL as the source of truth for your SPA view, so a refresh or a pasted link lands on the right screen.
- Remember that
pushState/replaceStatenever firepopstate; call your render function yourself after pushing.