A tiny URL router for Nano Stores state manager.
- Small. 685 bytes (minified and brotlied). Zero dependencies.
- Good TypeScript support.
- Framework agnostic. Can be used with React, Preact, Vue, Svelte, Angular, Solid.js, and vanilla JS.
Since Nano Stores promote moving logic to store, the router is a store, not a component in UI framework like React.
// stores/router.ts
import { createRouter } from '@nanostores/router'
export const $router = createRouter({
home: '/',
list: '/posts/:category',
post: '/posts/:category/:post'
})
Store in active mode listen for <a>
clicks on document.body
and Back button
in browser.
// components/layout.tsx
import { useStore } from '@nanostores/react'
import { $router } from '../stores/router.js'
export const Layout = () => {
const page = useStore($router)
if (!page) {
return <Error404 />
} else if (page.route === 'home') {
return <HomePage />
} else if (page.route === 'list') {
return <ListPage category={page.params.category} filters={page.search} />
} else if (page.route === 'post') {
return <PostPage post={page.params.post} />
}
}
Made in Evil Martians, product consulting for developer tools.
npm install nanostores @nanostores/router
See Nano Stores docs about using the store and subscribing to store’s changes in UI frameworks.
Routes is an object of route’s name to route pattern:
createRouter({
route1: '/',
route2: '/path/:var1/and/:var2',
route3: [/\/posts\/(draft|new)\/(\d+)/, (type, id) => ({ type, id })]
})
For string patterns you can use :name
for variable parts. To make the
parameter optional, mark it with the ?
modifier:
createRouter({
routeName: '/profile/:id?/:tab?'
})
Routes can have RegExp patterns. They should be an array with function,
which convert ()
groups to key-value map.
For TypeScript, router parameters will be converted to types automatically. You need to use TypeScript ≥5.x.
createRouter({
routeName: '/path/:var1/and/:var2',
routeName2: [/path2/, () => ({ num: 1, str: '' })]
})
/**
* Params will be inferred as:
* {
* routeName: { var1: string, var2: string },
* routeName2: { num: number, str: string }
* }
*/
Router value contains parsed ?a=1&b=2
search values:
location.href = '/posts/general?sort=name'
router.get() //=> {
// path: '/posts/category',
// route: 'list',
// params: { category: 'general' },
// search: { sort: 'name' }
// }
To use search query like ?a=1&b=2
in routes you need to set search
option:
createRouter({
home: '/p/?page=home'
}, {
search: true
})
Router will works with ?search
part as a string. Parameters order will
be critical.
By default, router and ?search
params store will add click
event listener
on window
to track links clicks.
To disable click tracking for specific link, add target="_self"
to link tag:
<a href="/posts" target="_self">Posts</a>
You can disable this behavior by links: false
options and create custom
<Link>
component.
export const $router = createRouter({ … }, { links: false })
function onClick (e) {
let link = event.target.closest('a')
if (isPjax(link, e)) {
$router.open(new Url(link.href).pathname)
}
}
export const Link = (props) => {
return <a onClick={onClick} {...props}></a>
}
Using getPagePath()
avoids hard coding URL in templates. It is better
to use the router as a single place of truth.
import { getPagePath } from '@nanostores/router'
…
<a href={getPagePath($router, 'post', { category: 'guides', post: '10' })}>
If you need to change URL programmatically you can use openPage
or redirectPage
:
import { openPage, redirectPage } from '@nanostores/router'
function requireLogin () {
openPage($router, 'login')
}
function onLoginSuccess() {
// Replace login route, so we don’t face it on back navigation
redirectPage($router, 'home')
}
All functions accept search params as last argument:
getPagePath($router, 'list', { category: 'guides' }, { sort: 'name' })
//=> '/posts/guides?sort=name'
Router can be used in Node environment without window
and location
.
In this case, it will always return route to /
path.
You can manually set any other route:
if (isServer) {
$router.open('/posts/demo/1')
}