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.

  • options object
    • mode - one of history, hash, memory, default is history
    • qs - 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.

  • routes an array of arrays of route definitions, where each route is an object of shape { path, redirect, routes, ...metadata }
    • path is the url pattern to match that can include named parameters as segments
    • redirect can be a string or a function that redirects upon entering that route
    • routes is a nested object of nested route definitions
    • ...metadata all other other keys can be chosen by you
  • onChange is called with the route object

route is an object of shape { url, pathname, params, query, search, hash, pattern, data }:

  • url full relative url string including query string and hash if any
  • pathname the pathname portion of the target url, which can include named segments
  • params params extracted from the named pathname segments
  • query query object that was parsed with qs.parse
  • search full unparsed query string
  • hash hash fragment
  • pattern the matched route pattern as defined in the route config
  • data an 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.

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 - a string url or an object with the following properties
    • url a relative url string or a route pattern
    • pathname the pathname portion of the target url, which can include named segments
    • params params to interpolate into the named pathname segments
    • query the query object that will be passed through qs.stringify
    • hash the hash fragment to append to the url of the url
    • replace set to true to replace the current entry in the navigation stack instead of pushing
    • merge set 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.

  • to object of shape { pathname, params, query, hash }. The params will be interpolated into the pathname if the pathname contains any parametrised segments. The query is an object that will be passed through qs.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.