Start with a URL object
The examples below run in a modern browser’s JavaScript console on a page you control. A query string is the part of a URL after ? and before any # fragment. It is useful for small pieces of shareable interface state, such as a search term, selected category, or page number. Rather than splitting the address into strings yourself, create a URL object: const url = new URL(window.location.href);. Its searchParams property provides methods for working with the query string.
To try a fixed example without changing the current page, run const url = new URL('https://example.com/products?category=books&page=2');. Then url.searchParams.get('category') returns 'books', and url.searchParams.get('page') returns '2'. Query values are strings, even when they look like numbers. If you need a number, convert and validate it: const page = Number(url.searchParams.get('page'));. Check that it is a suitable positive integer before using it for pagination.
Read missing, empty, and repeated values
The get() method returns the first value for a name, or null when that name is absent. An empty value is different: for ?category=, get('category') returns an empty string. Use url.searchParams.has('category') when you need to distinguish an absent parameter from one that is present but empty. Parameter names are case-sensitive, so page and Page are separate names.
Query strings can contain the same name more than once. For ?tag=css&tag=javascript, get('tag') returns 'css', while getAll('tag') returns both values as an array. This makes getAll() a good fit for multi-select filters. Decide how your interface should treat repeated keys before reading them; otherwise, a control that displays one selected value may not reflect the full URL.
URLSearchParams also handles decoding. For example, a value encoded with %20 is returned with a space. Avoid manually decoding the result of get(), since it is already decoded. When reading optional values, provide a deliberate fallback: const term = url.searchParams.get('q') ?? '';. The nullish coalescing operator keeps an intentionally empty q value empty instead of replacing it.
Change parameters without hand-building a query string
Work on a URL object when you want to preserve its path and fragment. Starting with const url = new URL(window.location.href);, run url.searchParams.set('category', 'books'); to add or replace a category. Run url.searchParams.delete('page'); to remove a page number that no longer applies after the filter changes. These operations update the URL object in memory; they do not immediately change the browser’s address bar.
Use append() when repeated values are intentional: url.searchParams.append('tag', 'javascript');. Unlike append(), set() replaces all existing values for that name with one value. To clear a multi-select filter, delete('tag') removes every tag entry. When generating a link from the modified object, assign link.href = url.href, where link is an anchor element. The URL object takes care of encoding values such as spaces and ampersands, so do not add escape sequences to values before passing them to set() or append().
For a URL that may be relative, provide a base: const url = new URL('/products?category=books', window.location.origin);. Calling new URL() with a relative address and no base throws an error. If you only have a standalone query string, new URLSearchParams('?page=2') is enough to read or modify its entries, but it has no path, hostname, or fragment to preserve.
Choose how the browser should use the result
To navigate to the updated address and let the page load, use window.location.assign(url.href). This is appropriate when the destination page should be rendered from the new URL. For an interface that already knows how to update itself, history.pushState(null, '', url) changes the address bar and adds a history entry without a page load. Use history.replaceState(null, '', url) instead when the change should replace the current entry, such as correcting an invalid default or avoiding a Back-button step for every keystroke.
A History API update changes the URL, not the visible controls or results. Update those separately from the same parsed state. Also, pushState() and replaceState() do not fire a popstate event when called. Listen for popstate to restore the interface when a visitor uses Back or Forward; in that handler, read a fresh URL from window.location.href. For example: window.addEventListener('popstate', () => { const current = new URL(window.location.href); renderFilters(current.searchParams); });. Here, renderFilters represents your own function for updating the controls and content.
Keep URL state predictable
Treat query parameters as input, not as guaranteed valid settings. Check allowed filter names and values, and supply defaults when a value is missing or malformed. If you put a search term into the page, insert it as text rather than HTML. Avoid storing secrets or private form data in query parameters: URLs can be copied, saved in browser history, and shared.
A useful design rule is to keep only state worth linking to in the URL. A category, search term, or page number often qualifies; a temporary hover state usually does not. Define one consistent mapping between controls and parameter names, then test it with missing values, empty values, repeated keys, spaces, and Back/Forward navigation. That small set of checks catches many query-string surprises before users encounter them.



