React Integration
This guide covers integrating @esmx/router with React. Unlike Vue, React does not need a separate integration package — the router's built-in micro-app system handles mounting, unmounting, and server-side rendering directly.
Installation
Install only the core router package:
No additional integration package is needed. React works through the router's apps callback, which provides mount, unmount, and renderToString lifecycle hooks.
Key Concepts
The React integration uses the micro-app pattern:
apps callback — Tells the router how to mount, unmount, and render your React app
mount(el) — Creates a React root and renders the app into a DOM element
unmount() — Unmounts the React root for cleanup
renderToString() — SSRs the app to an HTML string
The router passes itself to the apps callback, so your React components can access it via props or React context.
Step-by-Step Setup
1. Define Your Routes
Routes are framework-agnostic — the same as Vue:
src/routes.ts
import type { RouteConfig } from '@esmx/router';
export const routes: RouteConfig[] = [
{
path: '/',
component: () => import('./layouts/MainLayout'),
children: [
{ path: '', component: () => import('./pages/Home') },
{ path: 'about', component: () => import('./pages/About') },
{
path: 'users/:id',
component: () => import('./pages/UserProfile'),
meta: { requiresAuth: true }
}
]
}
];
2. Create the Router Context
Set up a React context so any component can access the router:
src/router-context.tsx
import { createContext, useContext, useState, useEffect } from 'react';
import type { Router, Route } from '@esmx/router';
interface RouterContextValue {
router: Router;
route: Route;
}
const RouterContext = createContext<RouterContextValue | null>(null);
export function RouterProvider({
router,
children
}: {
router: Router;
children: React.ReactNode;
}) {
const [route, setRoute] = useState(router.route);
useEffect(() => {
return router.afterEach((to) => {
setRoute(to);
});
}, [router]);
return (
<RouterContext.Provider value={{ router, route }}>
{children}
</RouterContext.Provider>
);
}
export function useRouter(): Router {
const context = useContext(RouterContext);
if (!context) {
throw new Error('useRouter must be used within a RouterProvider');
}
return context.router;
}
export function useRoute(): Route {
const context = useContext(RouterContext);
if (!context) {
throw new Error('useRoute must be used within a RouterProvider');
}
return context.route;
}
3. Create the Root Component
Build the app root that renders matched route components:
src/App.tsx
import { RouterProvider, useRoute } from './router-context';
import type { Router } from '@esmx/router';
function AppContent() {
const route = useRoute();
const Component = route.matched[0]?.component;
return Component ? <Component /> : <div>Not Found</div>;
}
export default function App({ router }: { router: Router }) {
return (
<RouterProvider router={router}>
<AppContent />
</RouterProvider>
);
}
4. Create a RouterLink Component
Use router.resolveLink() to build a navigation link component:
src/components/RouterLink.tsx
import type { ReactNode, MouseEvent } from 'react';
import { useRouter } from '../router-context';
interface RouterLinkProps {
to: string;
activeClass?: string;
className?: string;
children: ReactNode;
}
export function RouterLink({ to, activeClass, className, children }: RouterLinkProps) {
const router = useRouter();
const link = router.resolveLink({ to, activeClass });
function handleClick(e: MouseEvent) {
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
e.preventDefault();
router.push(to);
}
return (
<a
href={link.attributes.href}
className={[className, link.attributes.class].filter(Boolean).join(' ')}
onClick={handleClick}
>
{children}
</a>
);
}
5. Client Entry
The client entry creates the router with the apps callback and uses createRoot for mounting:
src/entry.client.tsx
import { Router, RouterMode } from '@esmx/router';
import { createRoot } from 'react-dom/client';
import { createElement } from 'react';
import App from './App';
import { routes } from './routes';
const router = new Router({
appId: 'app',
mode: RouterMode.history,
routes,
apps: (router) => {
let root = null;
return {
mount(el) {
root = createRoot(el);
root.render(createElement(App, { router }));
},
unmount() {
root?.unmount();
root = null;
},
async renderToString() {
const { renderToString } = await import('react-dom/server');
return renderToString(createElement(App, { router }));
}
};
}
});
The apps callback:
mount(el) — Called when the router needs to render the app. Creates a React root and renders the app internally.
unmount() — Called when the app should be torn down. Unmounts the React root for cleanup.
renderToString() — Called on the server for SSR. Returns the HTML string.
6. Server Entry (SSR)
src/entry.server.tsx
import type { RenderContext } from '@esmx/core';
import { Router, RouterMode } from '@esmx/router';
import { createElement } from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
import { routes } from './routes';
export default async (rc: RenderContext) => {
const router = new Router({
mode: RouterMode.memory,
base: new URL(rc.params.url, 'http://localhost'),
routes,
apps: (router) => ({
mount(el) { /* client only */ },
unmount() { /* client only */ },
async renderToString() {
return renderToString(createElement(App, { router }));
}
})
});
await router.replace(rc.params.url);
const html = await router.renderToString();
rc.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
${rc.preload()}
${rc.css()}
</head>
<body>
<div id="app">${html}</div>
${rc.importmap()}
${rc.moduleEntry()}
${rc.modulePreload()}
</body>
</html>`;
};
7. Node Entry
Use createRspackReactApp from @esmx/rspack-react for React-specific build tooling:
src/entry.node.ts
import http from 'node:http';
import type { EsmxOptions } from '@esmx/core';
export default {
async devApp(esmx) {
return import('@esmx/rspack-react').then((m) =>
m.createRspackReactApp(esmx)
);
},
async server(esmx) {
const server = http.createServer((req, res) => {
esmx.middleware(req, res, async () => {
const rc = await esmx.render({
params: { url: req.url }
});
res.end(rc.html);
});
});
server.listen(3000, () => {
console.log('Server started: http://localhost:3000');
});
}
} satisfies EsmxOptions;
createRspackReactApp configures Rspack with JSX/TSX support, React Refresh for HMR, and proper SSR bundling.
Using the Router in Components
Navigation
Use the useRouter hook to navigate programmatically:
src/pages/UserProfile.tsx
import { useRouter, useRoute } from '../router-context';
export default function UserProfile() {
const router = useRouter();
const route = useRoute();
const userId = route.params.id;
return (
<div>
<h1>User {userId}</h1>
<p>Current path: {route.path}</p>
<p>Query: {JSON.stringify(route.query)}</p>
<button onClick={() => router.push('/')}>
Go Home
</button>
<button onClick={() => router.replace('/about')}>
Replace with About
</button>
<button onClick={() => router.back()}>
Go Back
</button>
</div>
);
}
Using RouterLink
src/layouts/MainLayout.tsx
import { RouterLink } from '../components/RouterLink';
import { useRoute } from '../router-context';
export default function MainLayout() {
const route = useRoute();
// Render children from matched routes
const ChildComponent = route.matched[1]?.component;
return (
<div>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/users/42" activeClass="nav-active">
User 42
</RouterLink>
</nav>
<main>
{ChildComponent ? <ChildComponent /> : null}
</main>
</div>
);
}
import { useRoute } from '../router-context';
function MyComponent() {
const route = useRoute();
// Current path
route.path // '/users/42'
// Route parameters
route.params // { id: '42' }
// Query string parameters
route.query // { tab: 'profile' }
// Route meta data
route.meta // { requiresAuth: true }
// Matched route configs (parent → child)
route.matched // RouteConfig[]
}
Project File Structure
A typical React + SSR project with @esmx/router:
src/
├── entry.node.ts # Node.js server setup, dev/build config
├── entry.server.tsx # SSR rendering logic
├── entry.client.tsx # Client-side mounting
├── router-context.tsx # React context for router (useRouter, useRoute)
├── routes.ts # Route definitions
├── App.tsx # Root component
├── components/
│ └── RouterLink.tsx # Navigation link component
├── layouts/
│ └── MainLayout.tsx # Layout with navigation
└── pages/
├── Home.tsx
├── About.tsx
└── UserProfile.tsx
What's Next?