Home/Learn/JavaScript & TypeScript/History & URLs — Client-Side Routing Underneath

History & URLs — Client-Side Routing Underneath

Intermediate
The Browser

The History API changes the URL without a page load, and URLSearchParams parses query strings. Together they are what every client-side router is built on.

Overview

A single-page application changes what is on screen without asking the server for a new document, but the URL still has to change — otherwise the back button, bookmarks and sharing all break. pushState does exactly that, and popstate tells you when the user navigates with back or forward. React Router wraps this, but knowing the primitives explains why a refresh 404s without server configuration, and why putting state in the URL is usually better than putting it in a component.

pushState and popstate

Change the URL, render the new view, and listen for the back button.

pushState, popstate, and the refresh 404
// Change the URL without a request
history.pushState({ page: 2 }, '', '/problems?page=2')
history.replaceState({}, '', '/problems')     // no new history entry

// The user pressed back or forward
window.addEventListener('popstate', (e) => {
  render(e.state)        // whatever you passed to pushState
})

// pushState does NOT fire popstate — render yourself after calling it
function navigate(url, state) {
  history.pushState(state, '', url)
  render(state)
}

// The refresh problem: /problems/two-sum has no file on the server.
// The server must return index.html for unknown paths, or a refresh 404s.
// (Vercel and Next handle this; a bare nginx config does not.)

URLSearchParams

Never parse a query string by hand. This handles encoding, repeats and ordering.

Parsing and building query strings
const params = new URLSearchParams(location.search)
params.get('page')                 // '2' — always a string
params.getAll('topic')             // ['arrays', 'graphs'] for repeats
params.has('difficulty')

params.set('page', '3')
params.delete('q')
params.toString()                  // 'page=3&topic=arrays'

// Building a URL properly, with encoding handled
const url = new URL('/problems', location.origin)
url.searchParams.set('q', 'two sum & more')    // encoded correctly
url.toString()      // '/problems?q=two+sum+%26+more'

// Reading into an object
Object.fromEntries(new URLSearchParams(location.search))

The URL as State

Filters and pagination belong in the URL. It makes the view shareable, bookmarkable, and survivable across a refresh.

Filters belong in the URL
// State in a component: lost on refresh, cannot be shared
const [difficulty, setDifficulty] = useState('easy')

// State in the URL: survives refresh, shareable, back button works
function setFilter(key, value) {
  const url = new URL(location.href)
  value ? url.searchParams.set(key, value) : url.searchParams.delete(key)
  url.searchParams.delete('page')          // reset paging when filtering
  history.pushState({}, '', url)
  render()
}

// The test: if a student sends a teammate their current view,
// does the teammate see the same thing? If not, the state is in
// the wrong place.

Key Points to Remember

  • 1pushState changes the URL without a request; it does not fire popstate, so you render yourself
  • 2popstate fires when the user presses back or forward, carrying the state you pushed
  • 3Client-side routes 404 on refresh unless the server returns index.html for unknown paths
  • 4URLSearchParams handles encoding, repeated keys and ordering — never parse a query string manually
  • 5Filters and pagination belong in the URL so a view can be shared, bookmarked and survive a refresh

Interview Questions

Sign in to ask Aria
1

How does a single-page app change the URL without reloading the page?

Medium
2

Why does refreshing a client-side route sometimes give a 404, and how is it fixed?

Medium
3

Why put filter state in the URL rather than in component state?

Medium

Ask Aria about History & URLs — Client-Side Routing Underneath

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…