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:
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-domApp.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>