Skip to content
JavaScript js browser 5 min read

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
PropertyMeaningWritable
hrefThe complete URLYes (navigates)
protocolScheme including :, e.g. https:Yes
host / hostnameDomain with / without portYes
pathnamePath after the hostYes
searchQuery string including ?Yes
hashFragment including #Yes
originprotocol + hostNo (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 pushState must be same-origin, or the browser throws a SecurityError. 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 / URL instead of hand-parsing location.search — they handle encoding and duplicate keys correctly.
  • Choose replace() / replaceState() (no new history entry) for redirects and transient state; use assign() / pushState() when the user should be able to go back.
  • Always store the data you need to re-render in the pushState state object, and read it back in your popstate handler.
  • Keep pushState URLs 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/replaceState never fire popstate; call your render function yourself after pushing.
Last updated June 1, 2026
Was this helpful?