Routing¶
Routing in AppRun is event-driven. When the URL changes, the AppRun router publishes an
AppRun event using the URL as the event name. Components (or plain app.on handlers)
subscribe to those events. There is no separate routing configuration — routing is just
the event pub-sub system applied to the URL.
How it works¶
The router listens to the browser and publishes events automatically:
- On
hashchange, it routeslocation.hash. - On
popstate, it routeslocation.pathname. - For path routing, it also intercepts same-origin
<a>clicks, callshistory.pushState, and routes the new path — so plain links just work, no extra code.
When the URL becomes #/contact, the router publishes the #/contact event. A component
that subscribes to #/contact reacts and renders itself. That's the whole mechanism.
class Contact extends Component {
view = () => <div>Contact</div>;
update = { '#/contact': state => state };
}
Hash routing vs. path routing¶
AppRun supports three URL styles, and auto-detects which mode to use:
| Style | Example event name | Notes |
|---|---|---|
| Hash | #contact |
Hash fragment, no slash |
| Hash-slash | #/contact |
Hash fragment with slash (recommended for SPAs) |
| Path | /contact |
Clean URLs via the History API |
If any # or #/ route handler is registered, AppRun uses hash routing and listens to
hashchange. Otherwise it uses path routing, listens to popstate, and intercepts
link clicks.
- Hash routing works anywhere with no server configuration — the server only ever sees
the page before the
#. It is the safest choice for static hosting and the interactive examples in these docs. - Path routing produces clean URLs (
/contact) but requires the server to serve yourindex.htmlfor every route (SPA fallback), so a deep-link reload likehttps://site/contactdoesn't 404.
Pick one style and use it consistently across your routes.
Route parameters¶
Routes can capture parameters with :param and a trailing * rest segment. Captured
values are passed to the event handler as arguments.
// #/users/123 -> id = "123"
app.on('#/users/:id', (state, id) => { /* ... */ });
// #/files/a/b/c -> rest = "a/b/c"
app.on('#/files/*', (state, rest) => { /* ... */ });
Hierarchical routing¶
If there is no exact (or pattern) match, the router walks up the path and fires the closest parent handler, passing the remaining segments as arguments.
For URL: /api/v1/users/123
Router tries: /api/v1/users/123 → /api/v1/users → /api/v1 → /api → 404
If a handler is registered for /api, it receives the leftover segments:
// matches /api/v1/users/123 -> args = 'v1', 'users', '123'
app.on('/api', (state, ...segments) => { /* ... */ });
The router stops before the root handlers (/, #, #/) and fires the 404 event instead,
so a missing route never accidentally activates the home page.
Programmatic navigation¶
Publish the built-in route event to navigate from code:
app.run('route', '#/contact');
For path routing this updates the History API and routes the new path.
Sub-directory deployments (basePath)¶
If your app is served from a sub-directory, set app.basePath. The router strips it before
matching, and adds it back when navigating, so your route names stay relative.
app.basePath = '/myapp';
// Navigation goes to /myapp/users/123
// Routing matches /users/123
Declarative routes with addComponents¶
app.addComponents maps routes directly to components (instances, classes, or functions
that return them), mounting each to a shared element.
import app from 'apprun';
import Home from './Home';
import About from './About';
app.addComponents('#pages', {
'#/': Home,
'#/about': About,
// lazy-loaded:
'#/contact': () => import('./Contact').then(m => m.default),
});
Unhandled routes (404)¶
When no handler matches a route, the router publishes ROUTER_404_EVENT, giving the app a
chance to degrade gracefully (for example, a 404 page).
import app, { Component, ROUTER_404_EVENT } from 'apprun';
// Log unmatched routes.
app.on(ROUTER_404_EVENT, (url) => console.error('No route handler for', url));
// Or render a not-found page.
class NotFound extends Component {
view = () => <h1>Page not found</h1>;
update = {
[ROUTER_404_EVENT]: state => state
};
}
new NotFound().mount('#pages');
The ROUTER_EVENT¶
After every routing attempt (matched or not), the router also publishes ROUTER_EVENT with
the route name and arguments. Subscribe to it for cross-cutting concerns such as analytics
or highlighting the active menu item.
import app, { ROUTER_EVENT } from 'apprun';
app.on(ROUTER_EVENT, (url, ...args) => console.log('routed to', url, args));
Replacing the default router¶
Routing is just app.route. To use a custom router, overwrite app.route and wire up the
browser events yourself:
import app, { ROUTER_EVENT } from 'apprun';
function myRouter(url) {
app.run(url);
app.run(ROUTER_EVENT, url);
}
app.route = myRouter;
document.addEventListener('DOMContentLoaded', () => {
window.onpopstate = () => myRouter(location.pathname);
myRouter(location.pathname);
});