Skip to content

Adding Routing

Routing in Rimitive is reactive state. The router tracks the current URL and matches it against your route definitions. You render different views by reacting to those matches.


Start with just route definitions and a router:

import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule } from '@rimitive/signals/extend';
import { createRouterModule } from '@rimitive/router';
// Define routes - pure data
const routes = [
{ id: 'home', path: '' },
{ id: 'about', path: 'about' },
];
// Compose with router
const svc = compose(
SignalModule,
ComputedModule,
createRouterModule(routes)
);
const { router } = svc;

The router gives you reactive signals:

router.currentPath(); // '/' or '/about'
router.matches(); // [{ id: 'home', pattern: '/', params: {}, path: '/' }]

Navigate programmatically:

router.navigate('/about');
router.currentPath(); // '/about'
router.matches(); // [{ id: 'about', pattern: '/about', params: {}, path: '/about' }]

That’s the core: routes in, reactive matches out.


Use match() to render different views based on the current route:

import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
import { createDOMAdapter } from '@rimitive/view/adapters/dom';
import { createElModule } from '@rimitive/view/el';
import { createMatchModule } from '@rimitive/view/match';
import { MountModule } from '@rimitive/view/deps/mount';
import { createRouterModule } from '@rimitive/router';
const routes = [
{ id: 'home', path: '' },
{ id: 'about', path: 'about' },
];
const adapter = createDOMAdapter();
const svc = compose(
SignalModule,
ComputedModule,
EffectModule,
createElModule(adapter),
createMatchModule(adapter),
MountModule,
createRouterModule(routes)
);
const { el, match, mount, router } = svc;

Now render based on the matched route:

// Page components
const Home = () => el('div')(
el('h1')('Home'),
el('p')('Welcome!')
);
const About = () => el('div')(
el('h1')('About'),
el('p')('Learn more about us.')
);
const NotFound = () => el('div')(
el('h1')('404'),
el('p')('Page not found.')
);
// Route → component mapping
const pages = {
home: Home,
about: About,
};
// App with routing
const App = () => el('div')(
match(router.matches, (matches) => {
const route = matches[0];
if (!route) return NotFound();
const Page = pages[route.id];
return Page ? Page() : NotFound();
})
);
const app = mount(App());
document.body.appendChild(app.element!);

When router.matches changes, match() swaps the rendered component.


Use Link for declarative navigation that works with the router:

import { Link } from '@rimitive/router/link';
const Nav = () => el('nav')(
Link({ href: '/' })('Home'),
Link({ href: '/about' })('About')
);
const App = () => el('div')(
Nav(),
el('main')(
match(router.matches, (matches) => {
const route = matches[0];
if (!route) return NotFound();
const Page = pages[route.id];
return Page ? Page() : NotFound();
})
)
);

Link renders an <a> tag that intercepts clicks and calls router.navigate() instead of doing a full page reload.

For programmatic navigation, use router.navigate() directly:

const Home = () => el('div')(
el('h1')('Home'),
el('button').props({
onclick: () => router.navigate('/about')
})('Go to About')
);

Capture dynamic segments with :param syntax:

const routes = [
{ id: 'home', path: '' },
{ id: 'products', path: 'products' },
{ id: 'product-detail', path: 'products/:id' },
];

When the URL is /products/123, the match includes the parameter:

router.navigate('/products/123');
router.matches();
// [{ id: 'product-detail', pattern: '/products/:id', params: { id: '123' }, path: '/products/123' }]

Use the params in your component:

const ProductDetail = (params: { id: string }) => el('div')(
el('h1')(`Product ${params.id}`),
el('p')('Product details here...')
);
// In the router match
match(router.matches, (matches) => {
const route = matches[0];
if (!route) return NotFound();
if (route.id === 'product-detail') {
return ProductDetail(route.params as { id: string });
}
const Page = pages[route.id];
return Page ? Page() : NotFound();
});

The router parses query strings into reactive signals:

// URL: /products?sort=price&category=electronics
router.search(); // '?sort=price&category=electronics'
router.query(); // { sort: 'price', category: 'electronics' }

React to query changes:

const Products = () => {
const sortOrder = computed(() => router.query().sort || 'name');
return el('div')(
el('h1')('Products'),
el('p')(computed(() => `Sorted by: ${sortOrder()}`)),
el('button').props({
onclick: () => router.navigate('/products?sort=price')
})('Sort by Price')
);
};

Use router.currentPath to style the active link:

const NavLink = (href: string, label: string) => {
const isActive = computed(() => router.currentPath() === href);
return Link({
href,
className: computed(() => isActive() ? 'nav-link active' : 'nav-link')
})(label);
};
const Nav = () => el('nav')(
NavLink('/', 'Home'),
NavLink('/about', 'About'),
NavLink('/products', 'Products')
);

Putting it all together:

import { compose } from '@rimitive/core';
import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';
import { createDOMAdapter } from '@rimitive/view/adapters/dom';
import { createElModule } from '@rimitive/view/el';
import { createMatchModule } from '@rimitive/view/match';
import { MountModule } from '@rimitive/view/deps/mount';
import { createRouterModule } from '@rimitive/router';
import { Link } from '@rimitive/router/link';
// Routes
const routes = [
{ id: 'home', path: '' },
{ id: 'about', path: 'about' },
{ id: 'products', path: 'products' },
{ id: 'product-detail', path: 'products/:id' },
];
// Service
const adapter = createDOMAdapter();
const svc = compose(
SignalModule,
ComputedModule,
EffectModule,
createElModule(adapter),
createMatchModule(adapter),
MountModule,
createRouterModule(routes)
);
const { el, match, mount, router, computed } = svc;
// Navigation
const NavLink = (href: string, label: string) =>
Link({
href,
className: computed(() =>
router.currentPath() === href ? 'active' : ''
)
})(label);
const Nav = () => el('nav')(
NavLink('/', 'Home'),
NavLink('/about', 'About'),
NavLink('/products', 'Products')
);
// Pages
const Home = () => el('div')(
el('h1')('Home'),
el('button').props({
onclick: () => router.navigate('/about')
})('Learn More')
);
const About = () => el('div')(
el('h1')('About'),
el('p')('We build things.')
);
const Products = () => el('div')(
el('h1')('Products'),
el('ul')(
el('li')(Link({ href: '/products/1' })('Product 1')),
el('li')(Link({ href: '/products/2' })('Product 2')),
el('li')(Link({ href: '/products/3' })('Product 3'))
)
);
const ProductDetail = (id: string) => el('div')(
el('h1')(`Product ${id}`),
el('button').props({
onclick: () => router.navigate('/products')
})('← Back')
);
const NotFound = () => el('div')(
el('h1')('404'),
Link({ href: '/' })('Go Home')
);
// App
const App = () => el('div')(
Nav(),
el('main')(
match(router.matches, (matches) => {
const route = matches[0];
if (!route) return NotFound();
// Just plain old javascript...
switch (route.id) {
case 'home': return Home();
case 'about': return About();
case 'products': return Products();
case 'product-detail': return ProductDetail(route.params.id);
default: return NotFound();
}
})
)
);
// Mount
const app = mount(App());
document.body.appendChild(app.element!);

Routes are data. Matching is reactive. Rendering is just match() on router.matches. Everything composes with the same patterns you’ve already learned.

Next: Server Rendering for rendering on the server with data loading.