Skip to content

Preact Routing — Client-Side Navigation with preact-router

DodaTech Updated 2026-06-28 5 min read

Learn how to implement client-side routing in Preact using preact-router for single-page application navigation with minimal configuration.

In this lesson, you'll understand preact-router, route matching, link components, and how to structure multi-page Preact applications.

What You'll Learn

How to install preact-router, define routes, navigate with Link components, handle dynamic route parameters, and create nested layouts.

Why It Matters

Client-side routing enables single-page applications where navigation doesn't reload the page. Users get faster transitions and smoother experiences.

Real-World Use

DodaZIP's settings panel uses preact-router to navigate between General, Security, and Compression settings tabs without page reloads, preserving the extraction state in the background.

flowchart TD
    A[App Shell] --> B[/ Home]
    A --> C[/about About]
    A --> D[/settings Settings]
    A --> E[/users/:id User Profile]
    B --> F[Route Matcher]
    C --> F
    D --> F
    E --> F
    style A fill:#673ab8,color:#fff
    style F fill:#4a148c,color:#fff

Installing preact-router

npm install preact-router

preact-router is a lightweight routing library built specifically for Preact, with no React dependencies.

Basic Routing

import { Router } from 'preact-router';
import { h, render } from 'preact';

import Home from './routes/Home';
import About from './routes/About';
import Contact from './routes/Contact';

function App() {
  return (
    <div>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
        <a href="/contact">Contact</a>
      </nav>
      <Router>
        <Home path="/" />
        <About path="/about" />
        <Contact path="/contact" />
        <div default>404  Page not found</div>
      </Router>
    </div>
  );
}

Output: Navigating to /about renders the About component without a page reload. The path prop on each component defines the URL it matches. The default prop creates a fallback for unmatched routes.

Use Link for accessible, state-preserving navigation:

import { Router, Link } from 'preact-router';

function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/contact">Contact</Link>
      <Link href="/users/42">User 42</Link>
    </nav>
  );
}

function App() {
  return (
    <div>
      <Nav />
      <Router>
        <Home path="/" />
        <About path="/about" />
        <Contact path="/contact" />
        <User path="/users/:id" />
      </Router>
    </div>
  );
}

Output: <Link> renders an <a> tag but intercepts clicks to prevent full page reloads. It updates the URL and renders the matching route component.

Dynamic Route Parameters

Access URL parameters in route components:

import { Router } from 'preact-router';

function User({ id }) {
  // The `id` prop comes from the :id segment in the path
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then(res => res.json())
      .then(setUser);
  }, [id]);

  if (!user) return <p>Loading user {id}...</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <Router>
      <User path="/users/:id" />
      <Home path="/" />
    </Router>
  );
}

Output: Navigating to /users/42 passes id="42" as a prop to the User component. The component fetches the user data and displays it.

Programmatic Navigation

Navigate imperatively with the router instance:

import { route } from 'preact-router';

function LoginForm() {
  const handleSubmit = async (e) => {
    e.preventDefault();
    const success = await login(/* ... */);
    if (success) {
      // Navigate to dashboard after login
      route('/dashboard');
    }
  };

  return <form onSubmit={handleSubmit}>/* ... */</form>;
}

The route() function navigates to a new URL without a full page reload. It accepts a path and an optional second parameter for state or replace behavior.

Nested Routes

preact-router supports nested routers for complex layouts:

import { Router } from 'preact-router';

function Settings() {
  return (
    <div style={{ display: 'flex' }}>
      <aside>
        <Link href="/settings/general">General</Link>
        <Link href="/settings/security">Security</Link>
        <Link href="/settings/notifications">Notifications</Link>
      </aside>
      <main>
        <Router>
          <General path="/settings/general" />
          <Security path="/settings/security" />
          <Notifications path="/settings/notifications" />
          <div default>Select a setting</div>
        </Router>
      </main>
    </div>
  );
}

function App() {
  return (
    <Router>
      <Home path="/" />
      <Settings path="/settings" />
      <Settings path="/settings/:page" />
    </Router>
  );
}

Output: Navigating to /settings/security renders the Settings layout with the Security sub-route. The nested router handles the second URL segment.

Route Matching and Order

preact-router matches routes in order. More specific routes should come first:

<Router>
  <Home path="/" />
  <UserList path="/users" />
  <User path="/users/:id" />
  <UserPosts path="/users/:id/posts" />
  <NotFound default />
</Router>

Output: /users matches UserList. /users/42 matches User. /users/42/posts matches UserPosts. The default route catches everything else.

Common Mistakes

  1. Forgetting to import Link from preact-router: Using regular <a> tags causes full page reloads, losing application state.
  2. Using wrong path syntax: Paths use colon-prefixed segments for params (:id), not React Router's :id? or * syntax.
  3. Placing routes in wrong order: preact-router returns the first match. Put more specific routes before less specific ones.
  4. Not handling the default route: Without a default, unmatched URLs render nothing, leaving users with a blank page.
  5. Calling route() on the server: preact-router uses the browser history API. Guard route() calls with typeof window !== 'undefined' during SSR.

Practice Questions

  1. How do you define a route with a dynamic parameter? Answer: Use colon syntax in the path prop: <User path="/users/:id" />. The parameter value is passed as a prop to the component.

  2. What does Link do differently from a regular <a> tag? Answer: Link intercepts clicks and uses the history API to navigate without a full page reload. It also ensures the router re-renders the correct component.

  3. How do you create a 404 fallback route? Answer: Add a component with the default prop instead of a path prop. It matches any URL that doesn't match other routes.

  4. What function navigates programmatically? Answer: route(path) from preact-router. It navigates without page reload and preserves application state.

Challenge

Build a blog with preact-router that has routes for: home (/), post list (/posts), individual post (/posts/:slug), and author page (/authors/:id). Use nested routes for the blog section.

Mini Project

Create a multi-tab documentation viewer with preact-router. Each tab (Installation, Usage, API, Examples) is a route. Use nested routing for API sub-sections.

FAQ

Can I use React Router instead of preact-router?

: Yes, through preact/compat. Install react-router-dom and import it normally with compat aliasing configured.

Does preact-router support hash routing?

: Yes. Import HashRouter from preact-router or use the useHash option.

Is preact-router compatible with SSR?

: Yes. preact-router works with server-side rendering. Use the url option to pass the initial URL from the server.

Does preact-router support route transitions?

: No built-in transitions. Use CSS animations or a library like preact-transition-group with preact-router.

What's Next

Learn about Preact Forms to handle form input, validation, and submission in Preact applications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro