npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@acdlite/router

v0.1.4

Published

An experiment in functional routing.

Downloads

16

Readme

Router

build status npm version

An experiment in functional routing for JavaScript applications.

npm install --save @acdlite/router

The name is intentionally generic because it's still in the experimental phase (perhaps forever).

Key features:

  • A router is defined using composable middleware functions.
  • A router is a function that turns a path into a state object. That's it. This allows for total separation of history management from route matching.
  • Because history management is separate, the server-side API is identical to the client-side API.

See below for a proof-of-concept that mimics the React Router API.

Should I use this?

No.

Well, maybe. I'm currently using this in a side project, but I wouldn't recommend it for any production apps.

How it works

A "router" in the context of this project is a function that accepts a path and a callback. The router turns the path into a state object by passing it through a series of middleware. Once the middleware completes, the callback is called (either synchronously or asynchronously) with the final state object, which can be used to render an app.

History management is considered a separate concern — just pass the router a string. On the client, use a project like history. On the server, use your favorite web framework like Express or Koa.

const router = createRouter(...middlewares)

router('/some/path', (error, state) => {
  // Render app using state
})

Middleware

A middleware is a function that accepts Node-style callback (we'll call it a listener) and returns a new Node-style callback with augmented behavior.

type Listener = (error: Error, state: Object) => void
type Middleware = (next: Listener) => Listener

An important feature of middleware is that they are composable:

// Middlewares 1, 2, and 3 will run in sequence from left to right
const combinedMiddleware = compose(middleware1, middleware2, middlware3)

Router middleware is much like middleware in Redux. It is used to augment a state object as it passes through a router. Here's an example of a middleware that adds a query field:

import queryString from 'query-string'

const parseQuery = next => (error, state) => {
  if (error) return next(error)

  next(null, {
    ...state,
    query: queryString.parse(state.search)
  })
}

As with React props and Redux state, we treat router state as immutable.

State object conventions

All state objects should have the fields path, pathname, search, and hash. When you pass a path string to a router function, the remaining fields are extracted from the path. The reverse also works: if instead of a path string you pass an initial state object to a router function with pathname, search, and hash, a path field is added. This allows middleware to depend on those fields without having to do their own parsing.

There are two additional fields which have special meanings: redirect and done. redirect is self-explanatory: a middleware should skip any state object with a redirect field by passing it to the next middleware. Similarly, a state object with done: true indicates that a previous middleware has already handled it, and it needs no further processing by remaining middleware. (There are some circumstances where it may be appropriate for a middleware to process a done state object.)

Handling all these special cases can get tedious. The handle() allows you to create a middleware that handles specific cases. It's a bit like a switch statement, or pattern matching. Example

import { handle } from '@acdlite/router'

const middleware = handle({
  // Handle error
  error: next => (error, state) => {...}

  // Handle redirect
  redirect: next => (error, state) => {...}

  // Handle done
  done: next => (error, state) => {...}

  // Handle all other cases
  next: next => (error, state) => {...}
})

next() is the most common handler.

If a handler is omitted, the default behavior is to pass the state object through to the next middleware, unchanged.

Proof-of-concept: React Router-like API

As a proof-of-concept, the react-router/ directory includes utilities for implementing a React Router-like API using middleware. It supports:

  • Nested route matching, with params
  • Plain object routes or JSX routes
  • Asynchronous route fetching, using config.getChildRoutes()
  • Asynchronous component fetching, using config.getComponent()
  • Index routes

Not yet completed:

  • <Redirect> routes

Internally, it uses several of React Router's methods, so the route matching behavior should be identical.

Example:

import { createRouter } from '@acdlite/router'
import { nestedRoute, getComponents, Route, IndexRoute } from '@acdlite/router/react-router'
import createHistory from 'history/lib/createBrowserHistory'

const reactRouter = createRouter(
  nestedRoute(
    <Route path="/" component={App}>
      <Route path="post">
        <IndexRoute component={PostIndex} />
        <Route path=":id" component={Post} />
      </Route>
    </Route>
  ),
  getComponents,
  // ... add additional middleware, if desired
)

const history = createHistory()

// Listen for location updates
history.listen(location => {
  // E.g. after navigating to '/post/123'
  // Routers can accept either a path string or an object with `pathname`,
  // `query`, and `search`, so we can pass the location object directly.
  reactRouter(location, {
    // Route was successful
    done: (error, state) => {
      // Returns a state object with info about the matched routes
      expect(state).to.eql({
        params: { id: '123' },
        routes: [...] // Array of matching route config objects
        components: [App, Post], // Array of matching components
        // ...plus other fields from the location object
      })

      // Render your app using state...
    },

    // Handle redirects
    redirect: (error, state) => {
      history.replace(state.redirect)
    },

    // Handle errors
    error: error => {
      throw error
    }
  }
})

A key thing to note is that the server-side API is exactly the same: instead of using history, just pass a path string directly to the router, and implement done(), redirect() and error() as appropriate.

Also note that there's no interdependency between history and your routing logic.

The router returns the matched components, but it's up to you to render them how you like. An easy way to start is using Recompose's nest() function:

const Component = nest(...state.components)
ReactDOM.render(<Component {...state.params} {...state.query} />)

That gets you 90% of the way to parity with React Router. Conveniences like the <Link> component and transition hooks would need to be re-implemented, but are fairly straightforward.