Pouter
Yet another javascript router. Minimalistic, universal, and framework agnostic.
Pouter provides an elegant way to respond to route changes on both the server and the client. On the client, Pouter relies on and is intended to be used in conjunction with history.
Pouter itself is a very small amount of code, and its only dependency is path-parser, which is also very small with no dependencies. The intended usage with history does add a little more size.
Basic Usage
/** Universal bits **/;const router = ; router;router;router;router; /** On the server **/app; /** On the client **/;const history = ; router;
Reasoning
Pouter takes the stance that a router is responsible for handling route changes*. Handling a route change can mean more than just changing the view or changing the application’s state. Although both of those things should indeed happen, there are also side effects to be considered. Some things need to happen before the new view is rendered - fetch data, check permissions, authorize, etc. But, perhaps most importantly, there are times when the view should not be rendered at all, and instead a redirect should occur.
To handle all this, each route is directed to a function (or “route handler”) to execute any arbitrary code you need it to. This is not so different from backbone router (or many other “old school” routers), except that the route handler is expected to indicate its outcome as one of: ok, error, or redirect. This enables Pouter to have a routeFinish
callback where both the client and the server have a place to perform a redirect, render a view, or handle an error... after any side effects are triggered by the route handler.
This stance differs from other popular routers which aim to automatically marry the route to a view or to the application’s state. That approach makes the mentioned side effects at least very awkward, if not a poor separation of concerns; it puts all of the responsibility on the view itself. The view has to fetch its own data, check if it is allowed to be viewed by the current user, and perform any necessary redirects. Although this is possible and solid architecture could make it feel manageable, it seems as though there has long been a place that is meant to handle these very concerns - the router.
*or just respond to a single route when speaking server-side.
History and Navigation
Client side routing is accomplished through the history library. Pouter aims to work with history, rather than abstract it, so it should be included as a separate dependency. Since Pouter relies on history to listen for route changes, all client-side navigation should be handled through history.
// To minimize the build, only import the implementation you need, for most:; // To navigate:history;history;
Detailed Usage
Instantiating a Router
;const router = ;
Defining Routes
router;router; // note this is placed first or else it would be matched to /posts/:postIdrouter;
Path strings are parsed and matched by library path-parser. Note that only the path (not the query) will be matched, meaning that both URLs '/foo'
and '/foo?b=ar'
will be matched to route '/foo'
.
Route Handlers
{ // arbitrary data can be passed back, wich will be available in the onRouteFinish callback ;}; { // `redirect` is a reserved key to indicate that a redirect should happen ;}; { // `error` is a reserved key to indicate an error occured while handling route ;}; { // don't need to fetch data on client if the server already did if preRouted return ; // const postId = locationparamspostId; storepromise ;};
- Each route handler is passed 4 arguments:
done
,location
,context
, andpreRouted
. - Each handler must call
done
once and only once. preRouted
is true only for the first route on client and is a convenient way to know that the server already handled the route.location
andcontext
covered below.
Location
// for route '/posts/:postId'// at URL '/posts/abc?foo=bar'// location will be: url: '/posts/abc?foo=bar' path: '/posts/abc' queryString: 'foo=bar' query: foo: 'bar' params: postId: 'abc'
The location object is sent to both route handlers and the onRouteFinish callback.
Context
// use `setContext()` to set a "context" object on the routerrouter;// and it will be passed to each route handler as the third argumentrouter
Routing
// on the server// routes just the given URLrouter; // on the client// listens for location changes and routes each onerouter;
For both methods, Pouter finds the first route that matches the current location and invokes the corresponding route handler. Once the route handler is finished, the onRouteFinish callback is invoked.
Route Finish Callback
// server sideapp;
The second argument to both route()
and startRouting()
is an onRouteFinish callback. It is invoked after the route handler, and is passed 4 arguments: location
, data
, redirect
, and error
. location
will always be present. Only one of data
, redirect
, or error
will be present at a time, and this indicates the outcome of the route change. data
will contain the object passed into done()
by the route handler (unless there is an error or redirect).
Putting it Together
/app/router.js
;; { const router = ; router; // a redux store, for example router; router; return router;};
/app/route-handlers/posts.js
{ // in RL: dispatch action to fetch data; update store before calling done() ;}; { ;};
/app/history.js
// makes the history a singleton for the app... will need it in React components to navigate;const history = __IS_CLIENT__ ? : null;
/server/index.js
;;; const app = ; app;
/client/index.js
;;; const store = ;const router = ; router;// render view/component
Example Project
For a reference implementation check out this repo. Particularly files: