Routing

hadars doesn't include a router — bring your own. Any router that works with React SSR will work. This page shows how to set up react-router-dom v6.

How it works

The server renders a single App component for every URL. hadars passes the current request URL as the location prop (via getInitProps or directly from the framework). Your router reads that location and renders the matching page — same on server and client.

react-router-dom v6 provides two router components for this:

ComponentUsed onWhy
StaticRouterserverReads location from a prop — no window required.
BrowserRouterclientReads from window.location and listens to History API events.

Because both routers resolve to the same URL (and therefore the same page component), the rendered DOM is identical and React's hydrateRoot succeeds without warnings.

Installation

npm install react-router-dom

App.tsx setup

Detect server vs client with typeof window === 'undefined' and render the appropriate router. The location prop comes from the hadars App contract — it equals req.pathname + req.search from the incoming request.

import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { StaticRouter } from 'react-router-dom/server';
import { type HadarsApp, type HadarsRequest } from 'hadars';

import Home from './pages/Home';
import About from './pages/About';
import Blog from './pages/Blog';
import NotFound from './pages/NotFound';

interface Props {
    serverTime: string;
}

const AppRoutes: React.FC<Props> = (props) => (
    <Routes>
        <Route path="/" element={<Home {...props} />} />
        <Route path="/about" element={<About />} />
        <Route path="/blog/:slug" element={<Blog />} />
        <Route path="*" element={<NotFound />} />
    </Routes>
);

const App: HadarsApp<Props> = ({ location, ...props }) => {
    const routes = <AppRoutes {...props} />;

    if (typeof window === 'undefined') {
        // Server — location comes from the incoming request
        return <StaticRouter location={location}>{routes}</StaticRouter>;
    }

    // Client — reads from window.location, listens to History API
    return <BrowserRouter>{routes}</BrowserRouter>;
};

export const getInitProps = async (_req: HadarsRequest): Promise<Props> => ({
    serverTime: new Date().toISOString(),
});

export default App;

Navigation

Use react-router-dom's Link and NavLink for client-side navigation — they call history.pushState internally, which BrowserRouter intercepts to re-render without a full page reload.

import { Link, NavLink } from 'react-router-dom';

const Nav = () => (
    <nav>
        {/* NavLink adds an "active" class when the route matches */}
        <NavLink to="/" end className={({ isActive }) => isActive ? 'active' : ''}>
            Home
        </NavLink>
        <NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>
            About
        </NavLink>

        {/* Link for plain navigation without active state */}
        <Link to="/blog/hello-world">Read post</Link>
    </nav>
);

Reading route params

Use useParams inside any component rendered by a Route. During SSR, slim-react renders the correct route via StaticRouter, so the params are available server-side too.

import { useParams } from 'react-router-dom';
import { useServerData } from 'hadars';

const Blog: React.FC = () => {
    const { slug } = useParams<{ slug: string }>();

    const post = useServerData(() => db.getPost(slug!));
    if (!post) return null;

    return (
        <article>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
        </article>
    );
};

404 pages

Use a wildcard path="*" route for unmatched URLs. Set the HTTP status code to 404 with HadarsHead status=404:

import { HadarsHead } from 'hadars';

const NotFound: React.FC = () => (
    <>
        <HadarsHead status={404}>
            <title>404 — Page not found</title>
        </HadarsHead>
        <h1>Page not found</h1>
    </>
);

// In AppRoutes:
<Route path="*" element={<NotFound />} />

Nested layouts

Use react-router-dom's nested routes with Outlet for shared layouts (e.g. a sidebar that only appears on doc pages):

import { Outlet } from 'react-router-dom';

const DocsLayout: React.FC = () => (
    <div style={{ display: 'flex' }}>
        <aside style={{ width: 220 }}>
            <NavLink to="/docs/getting-started">Getting Started</NavLink>
            <NavLink to="/docs/api">API Reference</NavLink>
        </aside>
        <main style={{ flex: 1 }}>
            <Outlet /> {/* child routes render here */}
        </main>
    </div>
);

// In AppRoutes:
<Route path="/docs" element={<DocsLayout />}>
    <Route path="getting-started" element={<GettingStarted />} />
    <Route path="api" element={<ApiReference />} />
</Route>