A tiny URL router for Nano Stores state manager.
- Small. 684 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 at 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\/(?<type>draft|new)\/(?<id>\d+)/
})
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 URL search params (like ?sort=name
):
createRouter({ home: '/posts/:category' })
location.href = '/posts/general?sort=name'
router.get() //=> {
// path: '/posts/general',
// route: 'list',
// params: { category: 'general' },
// search: { sort: 'name' }
// }
To disable the automatic parsing of search params in routes you need to set search
option.
Router will now treat search query like ?a=1&b=2
as a string. Parameters order will be critical.
createRouter({ home: '/posts?page=general' }, { search: true })
location.href = '/posts/?page=general'
router.get() //=> {
// path: '/posts?page=general',
// route: 'list',
// params: { },
// search: { }
// }
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) {
e.preventDefault()
$router.open(new Url(e.target.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')
}