├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── compositor.json ├── demo ├── About.js ├── App.js ├── Detail.js ├── Index.js ├── entry.js ├── index.html └── node-server.js ├── karma.conf.js ├── karma.js ├── package.json ├── src ├── Link.js ├── Router.js ├── create-history.js ├── createRouter.js └── index.js ├── test └── Router.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | demo/bundle.js 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/react-context-router/51c9741180c40d46e4e447d308d4285b356d8289/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.2 4 | addons: 5 | firefox: '39.0' 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-context-router 3 | 4 | Minimal React router based on React context 5 | 6 | [![Build Status](https://travis-ci.org/jxnblk/react-context-router.svg?branch=master)](https://travis-ci.org/jxnblk/react-context-router) 7 | 8 | ```sh 9 | npm i react-context-router 10 | ``` 11 | 12 | [React Router](https://github.com/reactjs/react-router) is an excellent routing solution, 13 | but sometimes it does a lot more than you need it to. 14 | This is intended to be a smaller option with a simpler API. 15 | 16 | ## Features 17 | - Small-ish package ~5KB 18 | - Simple API 19 | - One higher order component and two components: Router & Link 20 | - Pass props directly to any component 21 | - Pass anything through route context 22 | 23 | ## Usage 24 | 25 | ```js 26 | // App.js 27 | import React from 'react' 28 | import { createRouter, Link } from 'react-context-router' 29 | 30 | const NotFound = () =>
Not Found
31 | 32 | const Nav = () => ( 33 | 37 | ) 38 | 39 | class App extends React.Component { 40 | render () { 41 | const { route } = this.props 42 | const Comp = route.component || NotFound 43 | 44 | return ( 45 |
46 |
49 | ) 50 | } 51 | } 52 | 53 | export default createRouter(App) 54 | ``` 55 | 56 | ```js 57 | // entry.js 58 | import React from 'react' 59 | import ReactDOM from 'react-dom' 60 | import App from './App' 61 | import Index from './Index' 62 | import Post from './Post' 63 | 64 | const routes = [ 65 | { 66 | path: '/', 67 | // Put any other route data here. 68 | // This object will be provided through 69 | // React context to the Router's children. 70 | component: Index 71 | }, 72 | { 73 | path: '/posts/:id', 74 | component: Post 75 | } 76 | ] 77 | 78 | const div = document.getElementById('app') 79 | 80 | ReactDOM.render(, div) 81 | ``` 82 | 83 | ## Server-side rendering 84 | 85 | Pass a `pathname` to the Router component to populate the context for a particular route. 86 | 87 | ```js 88 | const React = require('react') 89 | const ReactDOM = require('react-dom') 90 | const h = React.createElement 91 | const App = require('./App') 92 | 93 | const routes = [ 94 | { 95 | path: '/', 96 | name: 'Index' 97 | }, 98 | { 99 | path: '/posts/:id', 100 | name: 'Post' 101 | } 102 | ] 103 | 104 | const html = ReactDOMServer.renderToString( 105 | h(App, { 106 | routes, 107 | // Pass a value to the pathname prop for server side rendering 108 | pathname: '/posts/32' 109 | }) 110 | ) 111 | ``` 112 | 113 | ## API 114 | 115 | ### `createRouter` 116 | 117 | Higher order component that provides history and route data through context. 118 | 119 | #### `routes` prop 120 | Array of route objects. Each route must include a `path`. 121 | Any other value added to the object will be provided through context as the `route` object when the location matches the path. 122 | 123 | #### `pathname` prop 124 | Manually pass in a pathname to set the current path for server-side rendering. 125 | 126 | #### Child Context 127 | The wrapped component provides the following objects as context to its children: 128 | - `history` wrapped `window.history` object with `listen` and `push` methods 129 | - `route` object from the matching item in the `routes` prop. When a path contains parameters they will be passed as `route.params`. 130 | 131 | ### `` component 132 | 133 | Component wrapped with `createRouter`, provided as a convenience. 134 | 135 | ### `` component 136 | 137 | Used in place of `` links to use client-side history navigation. 138 | 139 | 140 | ### Size comparison 141 | 142 | Gzip: 143 | - react-context-router: 19.04 KB (5.13 KB without React) 144 | - react-router: 40.29 KB with React 145 | - Baseline react 14.52 KB 146 | 147 | *Results from [bundle-size](https://npmjs.com/package/bundle-size)* 148 | 149 | MIT License 150 | 151 | -------------------------------------------------------------------------------- /compositor.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jxnblk/react-context-router", 3 | "version": "0.1.4", 4 | "libraries": { 5 | "xv": "^1.1.19" 6 | }, 7 | "title": "React Context Router", 8 | "branch": "", 9 | "style": { 10 | "name": "Brutalist", 11 | "componentSet": { 12 | "nav": "nav/BasicNav", 13 | "header": "header/BasicHeader", 14 | "article": "article/MarkdownArticle", 15 | "footer": "footer/BasicFooter" 16 | }, 17 | "fontFamily": "Consolas, \"Liberation Mono\", Menlo, Courier, monospace", 18 | "heading": {}, 19 | "typeScale": [ 20 | 48, 21 | 32, 22 | 20, 23 | 18, 24 | 16, 25 | 14, 26 | 12 27 | ], 28 | "layout": { 29 | "maxWidth": 1024, 30 | "fluid": true 31 | }, 32 | "colors": { 33 | "text": "#333", 34 | "background": "#fff", 35 | "primary": "#666", 36 | "secondary": "#888", 37 | "highlight": "#1f80ff", 38 | "muted": "#f6f6f6", 39 | "border": "#eee" 40 | } 41 | }, 42 | "content": [ 43 | { 44 | "component": "nav", 45 | "links": [ 46 | { 47 | "href": "https://github.com/jxnblk/react-context-router", 48 | "text": "GitHub" 49 | }, 50 | { 51 | "href": "https://npmjs.com/package/react-context-router", 52 | "text": "npm" 53 | } 54 | ] 55 | }, 56 | { 57 | "component": "header", 58 | "heading": "react-context-router", 59 | "subhead": "Minimal React router based on React context", 60 | "children": [ 61 | { 62 | "component": "ui/TweetButton", 63 | "text": "react-context-router: Minimal React router based on React context", 64 | "url": null 65 | }, 66 | { 67 | "component": "ui/GithubButton", 68 | "user": "jxnblk", 69 | "repo": "react-context-router" 70 | } 71 | ], 72 | "text": "v1.1.0" 73 | }, 74 | { 75 | "component": "article", 76 | "metadata": { 77 | "source": "github.readme" 78 | }, 79 | "html": "\n

Minimal React router based on React context

\n

\n
npm i react-context-router

React Router is an excellent routing solution,\nbut sometimes it does a lot more than you need it to.\nThis is intended to be a smaller option with a simpler API.

\n

Features

\n\n

Usage

\n
// App.js\nimport React from 'react'\nimport { createRouter, Link } from 'react-context-router'\n\nconst NotFound = () => <div>Not Found</div>\n\nconst Nav = () => (\n  <nav>\n    <Link href='/' children='Home' />\n    <Link href='/posts/1' children='First Post' />\n  </nav>\n)\n\nclass App extends React.Component {\n  render () {\n    const { route } = this.props\n    const Comp = route.component || NotFound\n\n    return (\n      <div>\n        <Nav />\n        <Comp />\n      </div>\n    )\n  }\n}\n\nexport default createRouter(App)
// entry.js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from './App'\nimport Index from './Index'\nimport Post from './Post'\n\nconst routes = [\n  {\n    path: '/',\n    // Put any other route data here.\n    // This object will be provided through\n    // React context to the Router's children.\n    component: Index\n  },\n  {\n    path: '/posts/:id',\n    component: Post\n  }\n]\n\nconst div = document.getElementById('app')\n\nReactDOM.render(<App routes={routes}>, div)

Server-side rendering

\n

Pass a pathname to the Router component to populate the context for a particular route.

\n
const React = require('react')\nconst ReactDOM = require('react-dom')\nconst h = React.createElement\nconst App = require('./App')\n\nconst routes = [\n  {\n    path: '/',\n    name: 'Index'\n  },\n  {\n    path: '/posts/:id',\n    name: 'Post'\n  }\n]\n\nconst html = ReactDOMServer.renderToString(\n  h(App, {\n    routes,\n    // Pass a value to the pathname prop for server side rendering\n    pathname: '/posts/32'\n  })\n)

API

\n

createRouter

\n

Higher order component that provides history and route data through context.

\n

routes prop

\n

Array of route objects. Each route must include a path.\nAny other value added to the object will be provided through context as the route object when the location matches the path.

\n

pathname prop

\n

Manually pass in a pathname to set the current path for server-side rendering.

\n

Child Context

\n

The wrapped component provides the following objects as context to its children:

\n\n

<Router /> component

\n

Component wrapped with createRouter, provided as a convenience.

\n

<Link /> component

\n

Used in place of <a> links to use client-side history navigation.

\n

Size comparison

\n

Gzip:

\n\n

Results from bundle-size

\n

MIT License

\n" 80 | }, 81 | { 82 | "component": "footer", 83 | "links": [ 84 | { 85 | "href": "https://github.com/jxnblk/react-context-router", 86 | "text": "GitHub" 87 | }, 88 | { 89 | "href": "https://github.com/jxnblk", 90 | "text": "jxnblk" 91 | } 92 | ] 93 | } 94 | ] 95 | } -------------------------------------------------------------------------------- /demo/About.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const About = () => { 5 | return ( 6 |
7 |

About

8 |
9 | ) 10 | } 11 | 12 | export default About 13 | 14 | -------------------------------------------------------------------------------- /demo/App.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { createRouter, Link } from '../src' 4 | 5 | const NotFound = () => ( 6 |
7 | 404 8 |
9 | ) 10 | 11 | 12 | class App extends React.Component { 13 | render () { 14 | const { history, route } = this.props 15 | 16 | const sx = { 17 | root: { 18 | fontFamily: '-apple-system, sans-serif' 19 | } 20 | } 21 | 22 | const Comp = route.component || NotFound 23 | 24 | return ( 25 |
26 |

App

27 | 28 | 29 | 30 | 31 |
32 | ) 33 | } 34 | } 35 | 36 | App.contextTypes = { 37 | history: React.PropTypes.object, 38 | route: React.PropTypes.object 39 | } 40 | 41 | export default createRouter(App) 42 | 43 | -------------------------------------------------------------------------------- /demo/Detail.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Detail = (props, context) => { 5 | const { params } = context.route 6 | return ( 7 |
8 |

Item Detail {params.id}

9 |
10 | ) 11 | } 12 | 13 | Detail.contextTypes = { 14 | route: React.PropTypes.object 15 | } 16 | 17 | export default Detail 18 | 19 | -------------------------------------------------------------------------------- /demo/Index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Link } from '../src' 4 | 5 | const Index = (props, { route }) => { 6 | const links = Array.from({ length: 16 }).map((a, i) => i) 7 | 8 | return ( 9 |
10 |

Index

11 |
    12 | {links.map(i => ( 13 |
  • 14 | 15 |
  • 16 | ))} 17 |
18 |
19 | ) 20 | } 21 | 22 | Index.contextTypes = { 23 | route: React.PropTypes.object 24 | } 25 | 26 | export default Index 27 | 28 | -------------------------------------------------------------------------------- /demo/entry.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { createRouter, Router } from '../src' 5 | import App from './App' 6 | import Index from './Index' 7 | import About from './About' 8 | import Detail from './Detail' 9 | 10 | const div = document.getElementById('app') 11 | 12 | const routes = [ 13 | { 14 | path: '/', 15 | component: Index 16 | }, 17 | { 18 | path: '/about', 19 | component: About 20 | }, 21 | { 22 | path: '/items/:id', 23 | component: Detail 24 | } 25 | ] 26 | 27 | ReactDOM.render(, div) 28 | 29 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | -------------------------------------------------------------------------------- /demo/node-server.js: -------------------------------------------------------------------------------- 1 | // Server-side rendering demo 2 | // Run `node demo/node-server.js` using Node ^v6 3 | 4 | const React = require('react') 5 | const ReactDOMServer = require('react-dom/server') 6 | const { Router, Link } = require('..') 7 | const h = React.createElement 8 | 9 | const App = (props, context) => { 10 | return ( 11 | h('div', {}, [ 12 | h(Link, { key: 0, href: '/' }, 'Home'), 13 | h('span', { key: 1 }, 'Current route:'), 14 | h('pre', { key: 2 }, JSON.stringify(context.route, null, 2)) 15 | ]) 16 | ) 17 | } 18 | App.contextTypes = { 19 | route: React.PropTypes.object 20 | } 21 | 22 | const routes = [ 23 | { 24 | path: '/', 25 | name: 'Index' 26 | }, 27 | { 28 | path: '/posts/:id', 29 | name: 'Post' 30 | } 31 | ] 32 | 33 | const html = ReactDOMServer.renderToString( 34 | h(Router, { routes, pathname: '/posts/32' }, [ 35 | h(App, { key: 0 }) 36 | ]) 37 | ) 38 | 39 | console.log(html) 40 | 41 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack') 3 | 4 | var configuration = { 5 | browsers: [ 6 | 'Firefox' 7 | ], 8 | 9 | singleRun: true, 10 | 11 | files: [ 12 | 'karma.js' 13 | ], 14 | 15 | frameworks: [ 16 | 'mocha' 17 | ], 18 | 19 | plugins: [ 20 | 'karma-firefox-launcher', 21 | 'karma-mocha', 22 | 'karma-mocha-reporter', 23 | 'karma-webpack' 24 | ], 25 | 26 | preprocessors: { 27 | 'karma.js': [ 'webpack' ] 28 | }, 29 | 30 | reporters: [ 'mocha' ], 31 | 32 | webpack: { 33 | module: { 34 | loaders: [ 35 | { 36 | test: /\.js$/, 37 | exclude: /node_modules/, 38 | loader: 'babel-loader' 39 | }, 40 | { 41 | test: /\.json$/, 42 | loader: 'json-loader' 43 | } 44 | ] 45 | }, 46 | plugins: [ 47 | new webpack.IgnorePlugin(/react\/lib\/ReactContext/), 48 | new webpack.IgnorePlugin(/react\/lib\/ExecutionEnvironment/) 49 | ] 50 | }, 51 | 52 | webpackMiddleware: { 53 | noInfo: true 54 | }, 55 | 56 | client: { 57 | mocha: { 58 | reporter: 'html', 59 | ui: 'bdd' 60 | } 61 | } 62 | } 63 | 64 | module.exports = function (config) { 65 | config.set(configuration) 66 | } 67 | 68 | -------------------------------------------------------------------------------- /karma.js: -------------------------------------------------------------------------------- 1 | 2 | const context = require.context('./test', true, /\.js$/) 3 | context.keys().forEach(context) 4 | 5 | module.exports = context 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-context-router", 3 | "version": "1.1.0", 4 | "description": "Minimal React router based on React context", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "mkdir -p dist && babel src --out-dir dist", 8 | "build": "NODE_ENV=production webpack -p --progress", 9 | "start": "webpack-dev-server --hot --inline --progress --colors", 10 | "size": "bundle-size ./dist --env production && bundle-size react-router --env production", 11 | "analyze": "webpack --json | webpack-bundle-size-analyzer", 12 | "mocha": "mocha test --compilers js:babel-register", 13 | "test": "karma start" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "react-component", 18 | "routing", 19 | "router", 20 | "higher-order-component" 21 | ], 22 | "author": "Brent Jackson", 23 | "license": "MIT", 24 | "peerDependencies": { 25 | "react": "^15.0.0" 26 | }, 27 | "dependencies": { 28 | "path-to-regexp": "^1.5.1" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.10.1", 32 | "babel-loader": "^6.2.4", 33 | "babel-preset-es2015": "^6.9.0", 34 | "babel-preset-react": "^6.5.0", 35 | "babel-preset-stage-0": "^6.5.0", 36 | "babel-register": "^6.9.0", 37 | "bundle-size": "^0.7.0", 38 | "enzyme": "^2.3.0", 39 | "expect": "^1.20.1", 40 | "json-loader": "^0.5.4", 41 | "karma": "^1.1.1", 42 | "karma-firefox-launcher": "^1.0.0", 43 | "karma-mocha": "^1.1.1", 44 | "karma-mocha-reporter": "^2.0.4", 45 | "karma-webpack": "^1.7.0", 46 | "mocha": "^2.5.3", 47 | "react": "^15.2.1", 48 | "react-addons-test-utils": "^15.2.1", 49 | "react-dom": "^15.2.1", 50 | "react-router": "^2.4.1", 51 | "webpack": "^1.13.1", 52 | "webpack-dev-server": "^1.14.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Link.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Link = ({ 5 | href, 6 | ...props 7 | }, { 8 | history, 9 | location 10 | }) => { 11 | const handleClick = (e) => { 12 | e.preventDefault() 13 | history.push(href) 14 | } 15 | 16 | return ( 17 | 20 | ) 21 | } 22 | 23 | Link.contextTypes = { 24 | history: React.PropTypes.object, 25 | location: React.PropTypes.object 26 | } 27 | 28 | export default Link 29 | 30 | -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | 2 | import createRouter from './createRouter' 3 | 4 | export default createRouter('div') 5 | 6 | -------------------------------------------------------------------------------- /src/create-history.js: -------------------------------------------------------------------------------- 1 | 2 | const createHistory = () => { 3 | let history = {} 4 | let listeners = [] 5 | 6 | history.listen = () => null 7 | history.push = () => null 8 | 9 | if (typeof document !== 'undefined') { 10 | const handlePopState = e => listeners.forEach(l => l(window.location)) 11 | 12 | history = window.history 13 | 14 | history.listen = (listener) => { 15 | if (typeof listener !== 'function') return 16 | 17 | listeners.push(listener) 18 | return () => listeners = listener.filter(item => item !== listener) 19 | } 20 | 21 | history.push = (pathname) => { 22 | history.pushState({}, null, pathname) 23 | handlePopState() 24 | } 25 | 26 | window.addEventListener('popstate', handlePopState) 27 | } 28 | 29 | return history 30 | } 31 | 32 | export default createHistory 33 | 34 | -------------------------------------------------------------------------------- /src/createRouter.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import pathToRegexp from 'path-to-regexp' 4 | import createHistory from './create-history' 5 | 6 | const createRouter = (Comp) => { 7 | class Router extends React.Component { 8 | constructor ({ routes, pathname }) { 9 | super() 10 | this.state = { 11 | history: {}, 12 | route: this.getRoute.bind(this)(routes, { pathname }) 13 | } 14 | this.unlisten = () => {} 15 | this.getRoute = this.getRoute.bind(this) 16 | } 17 | 18 | getChildContext () { 19 | return this.state 20 | } 21 | 22 | componentDidMount () { 23 | const history = createHistory() 24 | const route = this.getRoute(this.props.routes, window.location) 25 | this.setState({ 26 | history, 27 | route 28 | }) 29 | this.unlisten = history.listen(this.handleHistoryChange.bind(this)) 30 | } 31 | 32 | componentWillUnmount () { 33 | this.unlisten() 34 | } 35 | 36 | handleHistoryChange (location) { 37 | const route = this.getRoute(this.props.routes, location) 38 | this.setState({ route }) 39 | } 40 | 41 | getRoute (routes, location) { 42 | const { pathname } = location 43 | const keys = [] 44 | const params = {} 45 | 46 | const route = routes.reduce((a, b) => { 47 | const reg = pathToRegexp(b.path, keys) 48 | const match = reg.test(pathname) 49 | if (match) { 50 | const tokens = reg.exec(pathname) 51 | keys.forEach((key, i) => { 52 | params[key.name] = tokens[i + 1] 53 | }) 54 | return b 55 | } 56 | return a 57 | }, null) || {} 58 | 59 | route.params = params 60 | return route 61 | } 62 | 63 | render () { 64 | return ( 65 | 69 | ) 70 | } 71 | } 72 | 73 | Router.propTypes = { 74 | pathname: React.PropTypes.string, 75 | routes: React.PropTypes.array 76 | } 77 | 78 | Router.defaultProps = { 79 | routes: [] 80 | } 81 | 82 | Router.childContextTypes = { 83 | history: React.PropTypes.object, 84 | route: React.PropTypes.object 85 | } 86 | 87 | return Router 88 | } 89 | 90 | export default createRouter 91 | 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Router from './Router' 3 | import Link from './Link' 4 | import createRouter from './createRouter' 5 | 6 | export { Router as Router } 7 | export { Link as Link } 8 | export { createRouter as createRouter } 9 | 10 | export default { 11 | createRouter, 12 | Router, 13 | Link 14 | } 15 | 16 | -------------------------------------------------------------------------------- /test/Router.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOMServer from 'react-dom/server' 4 | import { shallow, mount } from 'enzyme' 5 | import expect from 'expect' 6 | import { Router } from '../src' 7 | 8 | const Index = () => ( 9 |

Index

10 | ) 11 | 12 | const Foo = () => ( 13 |

Foo

14 | ) 15 | 16 | const Bar = (props, context) => ( 17 |

Bar {context.route.params.id}

18 | ) 19 | Bar.contextTypes = { 20 | route: React.PropTypes.object 21 | } 22 | 23 | let ctx 24 | const App = (props, context) => { 25 | ctx = context 26 | const Comp = context.route.component || 'div' 27 | 28 | return 29 | } 30 | 31 | App.contextTypes = { 32 | history: React.PropTypes.object, 33 | route: React.PropTypes.object 34 | } 35 | 36 | const routes = [ 37 | { 38 | path: '/', 39 | component: Index 40 | }, 41 | { 42 | path: '/foo', 43 | component: Foo 44 | }, 45 | { 46 | path: '/foo/:id', 47 | component: Bar 48 | } 49 | ] 50 | 51 | 52 | describe('', () => { 53 | describe('client-side rendering', () => { 54 | if (typeof document === 'undefined') { 55 | return 56 | } 57 | let root 58 | let app 59 | 60 | it('should mount', () => { 61 | mount() 62 | }) 63 | 64 | it('should not throw', () => { 65 | expect(() => { 66 | root = mount() 67 | app = root.find(App) 68 | }).toNotThrow() 69 | }) 70 | 71 | it('should pass context to child components', () => { 72 | expect(ctx).toBeAn('object') 73 | }) 74 | 75 | it('should pass history context', () => { 76 | expect(ctx.history).toBeAn('object') 77 | expect(ctx.history.push).toBeA('function') 78 | }) 79 | 80 | it('should pass route context', () => { 81 | expect(ctx.route).toBeAn('object') 82 | }) 83 | 84 | it('should have route params', () => { 85 | expect(ctx.route.params).toBeAn('object') 86 | }) 87 | 88 | context('when changing routes', () => { 89 | let prevCtx 90 | 91 | before(() => { 92 | prevCtx = ctx 93 | ctx.history.push('/foo/32') 94 | }) 95 | 96 | it('should change the route context', () => { 97 | expect(prevCtx.route !== ctx.route).toBe(true) 98 | }) 99 | 100 | it('should have a route param', () => { 101 | expect(ctx.route.params.id).toEqual(32) 102 | }) 103 | }) 104 | }) 105 | 106 | describe('getRoute()', () => { 107 | const instance = new Router({ routes }) 108 | const route = instance.getRoute(routes, { pathname: '/foo/32' }) 109 | 110 | it('should return a matched route', () => { 111 | expect(route).toBeAn('object') 112 | }) 113 | 114 | it('should return params', () => { 115 | expect(route.params.id).toEqual(32) 116 | }) 117 | }) 118 | 119 | describe('server-side rendering', () => { 120 | let html 121 | 122 | it('should not throw', () => { 123 | expect(() => { 124 | html = ReactDOMServer.renderToString( 125 | 126 | 127 | 128 | ) 129 | }).toNotThrow() 130 | }) 131 | 132 | it('should render the given route', () => { 133 | expect(html).toMatch(/Foo/) 134 | expect(html).toNotMatch(/Index/) 135 | }) 136 | 137 | it('should pass url params', () => { 138 | html = ReactDOMServer.renderToString( 139 | 140 | 141 | 142 | ) 143 | expect(html).toMatch(/Bar.+32/) 144 | expect(html).toNotMatch(/Index|Foo/) 145 | }) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require('webpack') 3 | 4 | const config = { 5 | entry: './demo/entry.js', 6 | output: { 7 | path: __dirname + '/demo', 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: /\.js$/, 14 | exclude: /node_modules/, 15 | loader: 'babel' 16 | } 17 | ] 18 | }, 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': { 22 | 'NODE_ENV': JSON.stringify('production') 23 | } 24 | }) 25 | ], 26 | devServer: { 27 | contentBase: 'demo', 28 | historyApiFallback: true 29 | } 30 | } 31 | 32 | module.exports = config 33 | 34 | --------------------------------------------------------------------------------