Universal Router

 

A simple middleware-style router for isomorphic JavaScript web apps

and single-page applications

 

v3.0.0 (2017-03-25)

 
import Router from 'universal-router';

const router = new Router([
  { path: '/one', action: () => 'Page One' },
  { path: '/two', action: () => 'Page Two' }
]);

router.resolve({ path: '/one' }).then(result => {
  document.body.innerHTML = result || 'Not found';
});

router.resolve({ path, ...context }) Promise<any>
Traverses the list of routes in the same order they were defined, finds the first route matching the provided URL path string and whose action method returns anything other than null or undefined

 

..where action is just a regular function that may, or may not, return any arbitrary data — a string, a React component, anything!

 

API

 

Child Routes

 

Each route may have an optional children: [ ... ] property containing the list of child routes:

 
const routes = {
  path: '/admin'
  children: [
    {
      path: '/', action: () => {/* ... */}        // www.example.com/admin
    },
    {
      path: '/users',
      children: [
        {
          path: '/', action: () => {/* ... */}    // www.example.com/admin/users
        },
        {
          path: '/:id', action: () => {/* ... */} // www.example.com/admin/users/123
        }
      ]
    }
  ]
};

Named route parameters are captured and added to context.params

 
const router = new Router({
  path: '/hello/:username',
  action: (context) => `Welcome, ${context.params.username}!`
});

router.resolve({ path: '/hello/john' })
  .then(result => console.log(result));
  // => Welcome, john!

URL Parameters

 

This functionality is powered by path-to-regexp npm module and works the same way as the routing solutions in many popular JavaScript frameworks such as Express and Koa. Also check out online router tester.

 

Context

 

In addition to a URL path string, any arbitrary data can be passed to the router's resolve() method, that becomes available inside action methods

 
const router = new Router({
  path: '/hello',
  action(context) {
    return `Welcome, ${context.user}!`
  }
});

router.resolve({ path: '/hello', user: 'admin' })
  .then(result => console.log(result));
  // => Welcome, admin!

Async Routes

 
const router = new Router({
  path: '/hello/:username',
  async action({ params }) {
    const resp = await fetch(`/api/users/${params.username}`);
    const user = await resp.json();
    if (user) return `Welcome, ${user.displayName}!`;
  }
});

router.resolve({ path: '/hello/john' })
  .then(result => console.log(result));
  // => Welcome, John Brown!

The router works great with asynchronous functions out of the box!

 

Use Babel to transpile your code with async / await to normal JavaScript. Alternatively, stick to ES6 Promises:

 
{
  path: '/hello/:username',
  action({ params }) {
    return fetch(`/api/users/${params.username}`)
      .then(resp => resp.json())
      .then(user => user && `Welcome, ${user.displayName}!`);
  }
}

Middlewares

 

Any route action function may act as a middleware by calling context.next()

 
const router = new Router({
  path: '/',
  async action({ next }) {
    console.log('middleware: start');
    const child = await next();
    console.log('middleware: end');
    return child;
  },
  children: [
    {
      path: '/hello',
      action() {
        console.log('route: return a result');
        return 'Hello, world!';
      }
    }
  ]
});

router.resolve({ path: '/hello' });

// Prints:
//   middleware: start
//   route: return a result
//   middleware: end

Use with React.js

 

Just create a middleware that's responsible for rendering React components

 
import Router from 'univeral-router';

const router = new Router({
  path: '/',
  async action({ next }) {
    const component = await next();
    return component && <Layout>{component}</Layout>;
  },
  children: [
    { path: '/', action: () => <HomePage /> },
    { path: '/about', action: () => <AboutPage /> }
  ]
});


router.resolve({ path: '/hello' }).then(component =>
  ReactDOM.render(component, document.getElementById('root'))
);

Use with Node.js / Express

 
import express from 'express';
import Router from 'universal-router';

const router = new Router({  // Note: The exact same routes can be used
  path: '/',                 // in both client-side and server-side apps
  async action({ next }) {
    const component = await next();
    return component && <Layout>{component}</Layout>;
  },
  children: [
    { path: '/', action: () => <HomePage /> },
    { path: '/about', action: () => <AboutPage /> }
  ]
});

const app = express();

app.use((req, res, next) => {
  router.resolve({ path: req.path }).then((component) => {
    const body = ReactDOM.renderToString(component);
    const html = `<html><head>...</head><body>${body}</body></html>`;
    res.send(html);
  }).catch(next);
});

app.listen(3000);

Use with React / Relay

 
import Router from 'universal-router';

const router = new Router({
  path: '/products/:id',
  action({ params }) {
    return {
      component: ProductPage, // <- Relay container
      params,                 // <- query parameters
      queries: {              // <- root queries
        viewer: () => Relay.QL`query { viewer }`,
        product: () => Relay.QL`query { product(id: $id) }`,
      }
    };
  }
});

router.resolve({ path: '/products/1' }).then(props =>
  ReactDOM.render(
    React.createElement(RelayApp, props),
    document.getElementById('app')
  )
);

Code Splitting

 

This routing approach works great with code splitting

 
const router = new Router([
  {
    path: '/',
    async action() {
      const Homepage = await import('./Homepage');
      return <Homepage />;
    }
  },
  {
    path: '/:username',
    async action ({ params }) {
      const [ UserProfile, data ] = await Promise.all([
        import('./UserProfile'),
        fetch(`/api/users/${params.username}`).then(x => x.json()),
      ]);

      if (data) return <UserProfile {...data} />;
    }
  }
]);

router.resolve({ path: '/john' }).then(/* renderer */);

Feedback?

 
Made with Slides.com