Space Router
Framework agnostic router for single page apps
Space Router packs all the features you need to keep your app in sync with the url. It’s distinct from many other routers in that there is only a single callback. This callback can be used to re-render your applocation, update a store and perform other actions on each url change. Space Router is stateless, it doesn’t store the current route leaving state completely up to you to handle.
In summary, Space Router:
- listens to url changes using popstate or hashchange event
- extracts url parameters and parses query strings
- supports nested routes and arbitrary route metadata
- fits into a wide range of application architectures and frameworks
- has no dependencies and weighs less than 2kb
Why?
Space Router was first created in 2017 to create a minimal library after learning from the various routing approaches used in Backbone, Ember and then React eras. Each new framework brought new requirements for a router, but each new router had a core that was very similar – listening to url changes, parsing url params and queries, performing navigations and generating links. Space Router was created as a framework agnostic router that should be easy enough to use standalone or to built on top of to create more sophisticated bindings for specific frameworks or applications architectures.
What about remix-run/history? History wraps the history API only. Space Router takes a more holistic approach with handling url and query parsing and declaring and matching route configurations to create a fully usable standalone router. It doesn’t have all the bells of whistles of the history package, but if you need them - use that instead.
See React Space Router for the official React bindings that provide a set of hooks and components to make space router a fully featured routing library for React.
Install
npm i space-router
Example
import Preact from 'preact'
import { createRouter } from 'space-router'
import { App, Home, Shows, Show, ShowSettings, NotFound } from './components'
// define your routes as a nested array of routes
const routes = [
{
component: App,
routes: [
{ path: '/', component: Home },
{ path: '/shows', component: Shows },
{
path: '/shows/:id',
component: Show,
routes: [{ path: '/shows/:id/settings', component: ShowSettings }],
},
],
},
{ path: '*', component: NotFound },
]
// create the router
const router = createRouter()
// start listening to the url changes and kick of the initial render
// based on the current url
export const dispose = router.listen(routes, render)
// every time the route changes, do something, e.g. re-render your app
// or implement more complex behaviours, such as fetching route components,
// route data, updating a store and so on, bind this to your application
// the way it makes sense for your framework and architecture
function render(route) {
// using reduceRight here to take the list of the nested components
// e.g. App > Home and render then right to left inside each other
const app = route.data.reduceRight((children, d) => {
const { component: Component } = d
return <Component params={route.params}>{children}</Component>
}, null)
Preact.render(app, document.body, document.body.lastElementChild)
}
API
createRouter
const router = createRouter(options)
Create the router object.
optionsobjectmode- one ofhistory,hash,memory, default ishistoryqs- a custom query string parser, an object of shape{ parse, stringify }
listen
const dispose = router.listen(routes, onChange)
Start listening to url changes. Every time the url changes via back/forward button or by performing programmatic navigations, the onChange callback will get called with the matched route object.
Note, calling listen will immediately call onChange based on the current url when in history or hash modes. This does not happen in memory mode so that you could perform the initial navigation yourself since there is no url to read from in that case.
routesan array of arrays of route definitions, where each route is an object of shape{ path, redirect, routes, ...metadata }pathis the url pattern to match that can include named parameters as segmentsredirectcan be a string or a function that redirects upon entering that routeroutesis a nested object of nested route definitions...metadataall other other keys can be chosen by you
onChangeis called with therouteobject
route is an object of shape { url, pathname, params, query, search, hash, pattern, data }:
urlfull relative url string including query string and hash if anypathnamethe pathname portion of the target url, which can include named segmentsparamsparams extracted from the named pathname segmentsqueryquery object that was parsed withqs.parsesearchfull unparsed query stringhashhash fragmentpatternthe matched route pattern as defined in the route configdataan array of nested matched route objects with any metadata found in the route config
Listen returns a dispose function that stops listening to url changes.
navigate
router.navigate(to)
// examples
router.navigate('/shows')
router.navigate({ url: '/show/1' })
router.navigate({ url: '/show/2', replace: true })
router.navigate({ pathname: '/shows', query: { 'most-recent': 1 } })
router.navigate({ query: { 'top-rated': 1 }, merge: true })
router.navigate({ query: { 'top-rated': undefined }, merge: true })
Navigate to a url. Navigating will update the browser’s location bar (unless in memory mode) and will call the router listener callback with the newly matched route.
to- astringurl or anobjectwith the following propertiesurla relative url string or a route patternpathnamethe pathname portion of the target url, which can include named segmentsparamsparams to interpolate into the named pathname segmentsquerythe query object that will be passed throughqs.stringifyhashthe hash fragment to append to the url of the urlreplaceset to true to replace the current entry in the navigation stack instead of pushingmergeset to true to merge in the params from the current url, alternatively set to the current route object to use that as the current route to be used in merging
Note, if url option is provided, the pathname, params, query and hash will be ignored.
Note, be careful when using merge as this reads the location’s current url which might be different from the one you store in your application’s state in case you’re performing async logic in the listen callback.
match
const route = router.match(url)
Match the url string against the routes and return the matching route object.
href
const url = router.href(to)
// examples
router.href('/shows')
router.href({ url: '/show/1' })
router.href({ pathname: '/shows', query: { 'most-recent': 1 } })
router.href({ query: { 'top-rated': 1 }, merge: true })
router.href({ query: { 'top-rated': undefined }, merge: true })
Create a relative url string to use in <a href> attribute.
toobject of shape{ pathname, params, query, hash }. Theparamswill be interpolated into the pathname if the pathname contains any parametrised segments. Thequeryis an object that will be passed throughqs.stringify.
Note: to can be a string, in which case href simply returns the input. Similarly, the to can contain { url } key in which case href returns that url. This is to align the function signature with that of navigate so the two can be used interchangeably.
getUrl
const url = router.getUrl()
Get the current url string. Note, this only includes the path and does not not include the protocol and host.
You shouldn’t need to read this most of the time since the updates to url changes and the matching route will be provided in the listen callback. Be especially careful if you’re performing asynchronous logic in your callback, such as lazily importing some modules, where you’re then constructing links based on the current url - use route provided to your listener instead of calling getUrl as the url might already have been updated to another value in the meantime.