scouter
v0.0.8
Published
700 byte framework agnostic router.
Downloads
2
Readme
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.
- Data first
- Async-by-default
- Nestable
- Middleware
- 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 aPromise
- any returned data will be passed to child-route
load
functions and output fromrouter.resolve()
- any returned data will be passed to child-route
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
document.title
usingoptions.title
utility- Incremental rendering using
picostate
- Redirect example
MIT