Neurological Phenomenon Multiplexer

    scouter

    0.0.8 • Public • Published

    scouter

    800 byte framework agnostic universal router.

    Features

    scouter is an attempt to take the magic out of routing front-end applications. Specifically, React. It's Not Components™, and hopefully that's a good thing.

    1. Data first
    2. Async-by-default
    3. Nestable
    4. Middleware
    5. Universal, works in Node

    scouter can be used with most UI building libraries, but the examples below will use React.

    Install

    npm i scouter --save

    Usage

    import React from 'react'
    import { render } from 'react-dom'
    import { router, route, use } from 'scouter'
     
    const app = router(
      use(context => {
        window.history.pushState({}, '', context.location)
      }),
      route({
        path: '/',
        component: props => <h1>Home</h1>
      }),
      route({
        path: '*',
        component: props => <h1>404 Not Found</h1>
      })
    )
     
    app.resolve('/').then(({ component: Comp }) => {
      render(<Comp />, document.body)
    })

    Defining routes

    Routes accept an object as their only parameter. The route object can contain the following properties:

    • path - a path representing the route, including named parameters (required)
    • component - a component to be returned when the route is matched (required)
    • load - called when route matches, useful for loading data, can return a Promise
      • any returned data will be passed to child-route load functions and output from router.resolve()
    • options - an object with extra data and parameters
      • not used internally (at the moment), but can be useful to library users
    import { route } from 'scouter'
     
    const path = '/'
     
    const component = props => (
      <h1>Home page</h1>
    )
     
    const load = context => {
      return new Promise((res, rej) => {
        resolve({})
      })
    }
     
    const options = {
      pageTitle: 'Home'
    }
     
    export default route({ path, component, load, options })

    Asynchronous routes

    When a load handler is defined on a route, scouter will wait to resolve the route until the routes's load function resolves. During this time, the developer can show a loading animation or what-have-you.

    Named route parameters are passed to the load function on the context object.

    import { route } from 'scouter'
     
    const path = '/posts/:slug'
     
    const component = post => (
      <h1>{post.slug}</h1>
    )
     
    const load = context => {
      return getPostFromAPI(context.params.slug)
    }
     
    export default route({ path, component, load })

    Nested routes

    Routes can be nested by passing a route to the function returned from the parent route. Nested routes build on the routes of their parents.

    This means that certain generic routes can be reused in different locations. The code below results in four separate routes: /about, /about/:slug, /posts, and /posts/:slug.

    import { router, route } from 'scouter'
     
    const About = route({
      path: '/about',
      component: About
    })
     
    const Posts = route({
      path: '/posts',
      component: Posts
    })
     
    const Lightbox = route({
      path: ':slug',
      component: Lightbox
    })
     
    export default router(
      About(
        Lightbox
      ),
      Posts(
        Lightbox
      )
    )

    Nested loading

    Nested routes will wait for their parent's load functions to resolve before calling their own. This is useful for cases where child routes depend on data from their parents.

    import { router, route } from 'scouter'
    import store from './my-store.js'
     
    const About = route({
      path: '/about',
      component: props => (
        <div>
          <h1>About page</h1>
     
          <ul>
            {props.teamMembers.map(person => (
              <li>{person.name}</li>
            ))}
          </ul>
        </div>
      ),
      load: context => {
        store.setState({
          teamMembers: [
            {
              name: 'Person One',
              slug: 'person-one'
            },
            {
              name: 'Person Two',
              slug: 'person-two'
            }
          ]
        })
      }
    })
     
    const AboutMember = route({
      path: ':slug',
      component: props => {
        const person = store.getState().teamMembers.filter(person => (
          person.slug === props.params.slug
        ))[0]
        return (
          <h1>{person.name}</h1>
        )
      }
    })
     
    const app = router (
      About(
        AboutMember
      )
    )
     
    app.resolve('/about/person-two').then(({ component: Comp, data, params }) => {
      render(<Comp params={params} />, document.body) // => <h1>Person Two</h1>
    })

    Middleware

    scouter implements very simple middleware. Middleware are applied for the route scope in which they are defined, and all nested scopes.

    Below, middleware1 will be called when the / route resolves. When the /posts/:slug route resolves, both middleware1 and middleware2 will be called.

    const middleware1 = use(context => console.log('Top level!'))
    const middleware2 = use(context => console.log('Nested!'))
     
    export default router(
      middleware1,
      Home,
      Posts(
        middleware2,
        Post
      )
    )

    Using context

    You can optionally pass an initial context object when calling router.resolve(). This is useful when dealing with API keys, authorization tokens, etc.

    router.resolve('/', { auth_token: 'abcde12345' }).then(props => ...)

    The context object is passed to all load functions, as well as any middleware you've defined along the way.

    const About = route({
      path: '/about',
      component: About,
      load: context => {
        console.log(context.auth_token) // => abcde12345
      }
    })

    Server Side Rendering

    const app = require('express')()
    const { renderToString } = require('react-dom/server')
     
    app.get('*', (req, res) => {
      router.resolve(req.originalUrl).then(({ component: Comp }) => {
        res.send(`
          <!doctype html>
          <html>
            <head>
            </head>
            <body>
              ${renderToString(<Comp />)}
            </body>
          </html>
        `)
      })
    })
     
    app.listen(8080)

    Recipes TODO

    1. document.title using options.title utility
    2. Incremental rendering using picostate
    3. Redirect example

    MIT

    Install

    npm i scouter

    DownloadsWeekly Downloads

    2

    Version

    0.0.8

    License

    MIT

    Last publish

    Collaborators

    • estrattonbailey