Router Overview
Xote includes a built-in signal-based router for building single-page applications (SPAs). The router uses the browser's History API and provides both imperative and declarative navigation.
Features
- Signal-based reactive routing
- Browser History API integration
- Pattern matching with dynamic parameters
- Imperative navigation (push/replace)
- Declarative routing components
- SPA navigation links (no page reload)
- Zero dependencies
Quick Start
1. Initialize the Router
Call Router.init() once at application startup:
open Xote
Router.init()
This sets the initial location from the browser URL and adds a popstate listener for back/forward button support.
2. Define Routes
Use Router.routes() to define your application routes:
let app = () => {
Component.div(
~children=[
Router.routes([
{
pattern: "/",
render: _params => homePage()
},
{
pattern: "/about",
render: _params => aboutPage()
},
{
pattern: "/users/:id",
render: params => userPage(~userId=params->Dict.get("id"), ())
},
])
],
()
)
}
Component.mountById(app(), "app")
3. Navigate
Use Router.push() or Router.link() to navigate:
// Imperative navigation
let goToAbout = (_evt: Dom.event) => {
Router.push("/about", ())
}
// Declarative links
Router.link(
~to="/about",
~children=[Component.text("About")],
()
)
The Location Signal
Router.location is a signal containing the current route information:
type location = {
pathname: string, // e.g., "/users/123"
search: string, // e.g., "?sort=name"
hash: string, // e.g., "#section"
}
Read it like any signal:
Effect.run(() => {
let currentLocation = Signal.get(Router.location)
Console.log2("Current path:", currentLocation.pathname)
})
Route Patterns
Patterns support static segments and dynamic parameters:
Static Routes
{pattern: "/", render: _params => homePage()}
{pattern: "/about", render: _params => aboutPage()}
{pattern: "/contact", render: _params => contactPage()}
Dynamic Parameters
Use :param syntax for dynamic segments:
{pattern: "/users/:id", render: params =>
switch params->Dict.get("id") {
| Some(id) => userPage(~userId=id, ())
| None => notFoundPage()
}
}
{pattern: "/posts/:postId/comments/:commentId", render: params => {
let postId = params->Dict.get("postId")
let commentId = params->Dict.get("commentId")
commentPage(~postId, ~commentId, ())
}}
Navigation Methods
Router.push()
Navigate to a new route with a new history entry:
Router.push("/users/123", ())
// With query string
Router.push("/search", ~search="?q=xote", ())
// With hash
Router.push("/docs", ~hash="#installation", ())
Router.replace()
Navigate without creating a new history entry:
Router.replace("/login", ())
This replaces the current history entry, so clicking the back button will skip this route.
Declarative Routing
Single Route
Render a component only when a pattern matches:
Router.route("/users/:id", params =>
userProfile(~userId=params->Dict.get("id"), ())
)
Multiple Routes
Render the first matching route from an array:
Router.routes([
{pattern: "/", render: _params => homePage()},
{pattern: "/about", render: _params => aboutPage()},
{pattern: "/users/:id", render: params => userPage(params)},
])
Only the first matching route renders. Order matters!
Navigation Links
Create links that navigate without page reload:
Router.link(
~to="/about",
~children=[Component.text("About Us")],
()
)
// With attributes
Router.link(
~to="/users/123",
~attrs=[Component.attr("class", "user-link")],
~children=[Component.text("View User")],
()
)
Complete Example
open Xote
// Initialize router
Router.init()
// Page components
let homePage = () => {
Component.div(~children=[
Component.h1(~children=[Component.text("Home")], ()),
Router.link(~to="/about", ~children=[Component.text("About")], ()),
], ())
}
let aboutPage = () => {
Component.div(~children=[
Component.h1(~children=[Component.text("About")], ()),
Router.link(~to="/", ~children=[Component.text("Home")], ()),
], ())
}
let userPage = (~userId: option<string>, ()) => {
Component.div(~children=[
Component.h1(~children=[
Component.text("User: " ++ Option.getOr(userId, "Unknown"))
], ()),
Router.link(~to="/", ~children=[Component.text("Home")], ()),
], ())
}
// Main app
let app = () => {
Component.div(
~children=[
// Navigation bar
Component.nav(~children=[
Router.link(~to="/", ~children=[Component.text("Home")], ()),
Component.text(" | "),
Router.link(~to="/about", ~children=[Component.text("About")], ()),
Component.text(" | "),
Router.link(~to="/users/123", ~children=[Component.text("User 123")], ()),
], ()),
// Routes
Component.hr(()),
Router.routes([
{pattern: "/", render: _params => homePage()},
{pattern: "/about", render: _params => aboutPage()},
{pattern: "/users/:id", render: params => userPage(~userId=params->Dict.get("id"), ())},
]),
],
()
)
}
Component.mountById(app(), "app")
How It Works
- Initialization:
Router.init()reads the current URL and sets up the location signal - History Integration: Listens to
popstateevents for back/forward navigation - Pattern Matching: Routes use simple string-based matching with
:paramsyntax - Reactive Rendering: Route components are wrapped in
SignalFragment+Computed - Link Handling:
Router.link()intercepts clicks and callsRouter.push()instead of following the href
Best Practices
- Initialize once: Call
Router.init()at the top level, not in components - Order routes carefully: More specific routes should come before generic ones
- Handle 404s: Add a catch-all route at the end for unmatched paths
- Use links for navigation: Prefer
Router.link()over manualRouter.push()calls - Extract parameters safely: Use
Optionmethods when accessing route parameters
Real-World Example
Check out the Functional Bookstore demo to see a complete e-commerce application using Xote Router:
- Multi-route navigation (catalog, cart, checkout, order confirmation)
- Active link highlighting based on current route
- Shopping cart state persisting across navigation
- Programmatic navigation after form submission
- Clean URL structure with SPA behavior
Next Steps
- Try the Demos to see routing in action
- Learn about Signals for reactive state
- Explore Components for building UIs
- Check the Getting Started guide for more information