Skip to main content
Saved

Client-Side Routing

Category Routing

Level Intermediate

Examples

React, Vue, Svelte, Native web

Handle navigation without full page reloads, so single-page apps keep their state between views.

In this article
  1. Problem
  2. Solution
  3. Example
  4. Basic Router Setup
  5. Scroll Position Management
  6. Hash-Based Routing
  7. Benefits
  8. Tradeoffs
  9. Summary
  10. See also

Client-Side Routing

Problem

Every click triggers a full page reload with all its consequences: the white flash, scroll jumping to top, form data disappearing, videos stopping, accordions collapsing. This is what web development looked like for decades, and it feels terrible compared to native apps where navigation is instant and everything stays put.

I’ve watched users lose their work because they accidentally clicked a link before saving. That spinning loading indicator on every navigation makes everything feel sluggish, even when content loads quickly.

Solution

Client-side routing avoids full page loads. The History API changes the URL without navigating, and the app swaps out only the content that needs to change. The router intercepts link clicks, calls pushState to update the URL, and renders the new route’s component; when users hit back, a popstate listener renders the previous route.

Everything outside the swapped content stays as it was (form state, scroll position, expanded accordions, playing media), because you’re swapping components in place rather than tearing down the entire page. Router libraries wrap this in declarative APIs: you map URL patterns to components, and the library handles the History API plumbing.

Example

Here’s client-side routing across frameworks: declarative route configuration, programmatic navigation, and scroll management.

Basic Router Setup

import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom';

// useNavigate() only works inside <BrowserRouter>, so it lives in a route component
function Login() {
  const navigate = useNavigate();

  const handleLogin = async (credentials) => {
    await login(credentials);
    navigate('/dashboard'); // Programmatic navigation
  };

  return <LoginForm onSubmit={handleLogin} />;
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/products">Products</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/products" element={<Products />} />
        <Route path="/login" element={<Login />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

Scroll Position Management

// Scroll to top on route change
function ScrollToTop() {
  const { pathname } = useLocation();
  useEffect(() => window.scrollTo(0, 0), [pathname]);
  return null;
}

// Or preserve scroll per route
const scrollPositions = new Map();

function preserveScroll() {
  scrollPositions.set(location.pathname, window.scrollY);
}

function restoreScroll(pathname) {
  window.scrollTo(0, scrollPositions.get(pathname) || 0);
}

Hash-Based Routing

// For environments without server config (URLs: example.com/#/about)
window.addEventListener('hashchange', () => {
  render(location.hash.slice(1) || '/');
});

Benefits

  • Faster navigation without the white flash and full reload on every click.
  • State persists across navigation: form data, scroll position, expanded UI, playing videos.
  • Animated transitions become possible since the DOM stays continuous between views.
  • Server load drops because you’re fetching data, not re-rendering entire HTML pages.
  • Precise control over route guards, programmatic navigation, and transition hooks.

Tradeoffs

  • Requires JavaScript: the entire system breaks without it, so server-side fallbacks are worth considering.
  • Server configuration needed: direct URL access must serve your app, not 404.
  • SEO complexity since crawlers may not execute JS; you might need SSR or prerendering.
  • Memory management falls on you: listeners and subscriptions need cleaning up when components unmount.
  • Manual scroll and focus handling: browsers do this automatically for page loads, but not for client-side transitions.
  • History quirks if you mix up pushState and replaceState, creating confusing back-button behavior.

Summary

Client-side routing intercepts link clicks, updates the URL via the History API, and swaps content without page reloads. The back button keeps working, and every view still has a URL people can share.

See also

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.