├── .gitignore ├── .npmignore ├── examples ├── basic │ ├── public │ │ ├── .gitignore │ │ └── index.html │ ├── .babelrc │ ├── containers │ │ ├── index.js │ │ ├── a.jsx │ │ ├── b.jsx │ │ ├── bDeep.jsx │ │ ├── c.jsx │ │ └── home.jsx │ ├── routes.js │ ├── store.js │ ├── webpack.config.js │ ├── app.jsx │ ├── index.js │ ├── viewSelector.jsx │ ├── components │ │ └── links.jsx │ └── package.json ├── authentication │ ├── .gitignore │ ├── public │ │ ├── .gitignore │ │ └── index.html │ ├── store.js │ ├── .babelrc │ ├── containers │ │ ├── index.js │ │ ├── me.jsx │ │ ├── user.jsx │ │ ├── users.jsx │ │ ├── home.jsx │ │ └── login.jsx │ ├── routes.js │ ├── reducers │ │ ├── index.js │ │ └── user.js │ ├── actions │ │ └── login.js │ ├── webpack.config.js │ ├── app.jsx │ ├── index.js │ ├── components │ │ └── links.jsx │ ├── package.json │ └── viewSelector.jsx └── minimalist │ ├── public │ ├── .gitignore │ └── index.html │ ├── .babelrc │ ├── webpack.config.js │ ├── package.json │ └── index.js ├── .eslintignore ├── .babelrc ├── test ├── .eslintrc └── index.js ├── images └── reroute-vs-redux-router.png ├── .travis.yml ├── .eslintrc ├── package.json ├── index.jsx └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | images 2 | examples 3 | test 4 | -------------------------------------------------------------------------------- /examples/basic/public/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/public/* 3 | -------------------------------------------------------------------------------- /examples/authentication/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/minimalist/public/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | -------------------------------------------------------------------------------- /examples/authentication/public/.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /images/reroute-vs-redux-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnaudRinquin/redux-reroute/HEAD/images/reroute-vs-redux-router.png -------------------------------------------------------------------------------- /examples/authentication/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import { rootReducer } from './reducers'; 3 | 4 | export default createStore(rootReducer); 5 | -------------------------------------------------------------------------------- /examples/basic/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "stage": 0 5 | }, 6 | "development": { 7 | "stage": 0 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/basic/containers/index.js: -------------------------------------------------------------------------------- 1 | export Home from './home'; 2 | 3 | export A from './a'; 4 | 5 | export B from './b'; 6 | export BDeep from './bDeep'; 7 | export C from './c'; 8 | -------------------------------------------------------------------------------- /examples/basic/routes.js: -------------------------------------------------------------------------------- 1 | export const home = `/`; 2 | 3 | export const a = `/a`; 4 | export const b = `/b`; 5 | export const bDeep = `/b/deep`; 6 | export const c = `/c/:myUrlParam`; 7 | -------------------------------------------------------------------------------- /examples/minimalist/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "stage": 0 5 | }, 6 | "development": { 7 | "stage": 0 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/authentication/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "stage": 0 5 | }, 6 | "development": { 7 | "stage": 0 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/authentication/containers/index.js: -------------------------------------------------------------------------------- 1 | export Home from './home'; 2 | 3 | export Login from './login'; 4 | 5 | export Users from './users'; 6 | export Me from './me'; 7 | export User from './user'; 8 | -------------------------------------------------------------------------------- /examples/authentication/routes.js: -------------------------------------------------------------------------------- 1 | export const home = `/`; 2 | 3 | export const users = `/users`; 4 | export const user = `${users}/:userId`; 5 | export const me = `${users}/me`; 6 | 7 | export const login = `/login`; 8 | -------------------------------------------------------------------------------- /examples/basic/containers/a.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default () => { 4 | return ( 5 |
6 |

Page A

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/basic/containers/b.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default () => { 4 | return ( 5 |
6 |

Page B

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/basic/containers/bDeep.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default () => { 4 | return ( 5 |
6 |

Page B / DEEP

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/authentication/containers/me.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default () => { 4 | return ( 5 |
6 |

The -Me- page

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | reroute basic example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/minimalist/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | reroute basic example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/authentication/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { location } from '../../../index'; 3 | 4 | import user from './user'; 5 | 6 | export const rootReducer = combineReducers({ 7 | user, 8 | location, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/basic/containers/c.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default ({myUrlParam}) => { 4 | return ( 5 |
6 |

Page C - with url param: {myUrlParam}

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/authentication/containers/user.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default ({userId}) => { 4 | return ( 5 |
6 |

{ `User ${userId} page`}

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /examples/basic/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import { location } from '../../index'; 3 | 4 | const rootReducer = combineReducers({ 5 | // you other reducers 6 | location, 7 | }); 8 | 9 | export default createStore(rootReducer); 10 | -------------------------------------------------------------------------------- /examples/authentication/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | reroute authentication example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/authentication/containers/users.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default () => { 4 | return ( 5 |
6 |

All Users page

7 | 8 |
Stuff goes here
9 | 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /examples/authentication/actions/login.js: -------------------------------------------------------------------------------- 1 | export const SUBMIT_LOGIN = 'SUBMIT_LOGIN'; 2 | export const SUBMIT_LOGOUT = 'SUBMIT_LOGOUT'; 3 | 4 | export function submitLogin() { 5 | return { 6 | type: SUBMIT_LOGIN, 7 | }; 8 | } 9 | 10 | export function submitLogout() { 11 | return { 12 | type: SUBMIT_LOGOUT, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | after_success: 15 | - npm run semantic-release 16 | branches: 17 | except: 18 | - "/^v\\d+\\.\\d+\\.\\d+$/" 19 | -------------------------------------------------------------------------------- /examples/basic/containers/home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default ({routingError}) => { 4 | return ( 5 |
6 |

reroute basic example homepage

7 | 8 | { routingError ? 9 |
{routingError}
: 10 | null 11 | } 12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /examples/authentication/containers/home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | 3 | export default ({routingError}) => { 4 | return ( 5 |
6 |

reroute example homepage

7 | 8 | { routingError ? 9 |
{routingError}
: 10 | null 11 | } 12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /examples/basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | bundle: [ 4 | './index.js', 5 | ], 6 | }, 7 | output: { 8 | path: 'dist', 9 | filename: '[name].js', 10 | }, 11 | resolve: { 12 | extensions: ['', '.js', '.jsx'], 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.jsx?$/, 18 | exclude: /node_modules/, 19 | loaders: ['babel'], 20 | }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/minimalist/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | bundle: [ 4 | './index.js', 5 | ], 6 | }, 7 | output: { 8 | path: 'dist', 9 | filename: '[name].js', 10 | }, 11 | resolve: { 12 | extensions: ['', '.js', '.jsx'], 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.jsx?$/, 18 | exclude: /node_modules/, 19 | loaders: ['babel'], 20 | }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/authentication/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | bundle: [ 4 | './index.js', 5 | ], 6 | }, 7 | output: { 8 | path: 'dist', 9 | filename: '[name].js', 10 | }, 11 | resolve: { 12 | extensions: ['', '.js', '.jsx'], 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.jsx?$/, 18 | exclude: /node_modules/, 19 | loaders: ['babel'], 20 | }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /examples/authentication/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | SUBMIT_LOGIN, 3 | SUBMIT_LOGOUT, 4 | } from '../actions/login'; 5 | 6 | export default function userReducer(state = {}, {type}) { 7 | switch (type) { 8 | case SUBMIT_LOGIN: 9 | return { 10 | ...state, 11 | authorized: true, 12 | }; 13 | case SUBMIT_LOGOUT: 14 | return { 15 | ...state, 16 | authorized: false, 17 | }; 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars 2 | import { connect } from 'react-redux'; 3 | import { viewSelector } from './viewSelector'; 4 | 5 | import { Links } from './components/links'; 6 | 7 | export class App extends Component { 8 | render() { 9 | const { component } = this.props; 10 | return
11 | 12 | { component } 13 |
; 14 | } 15 | } 16 | 17 | @connect(viewSelector) 18 | export default class ConnectedApp extends App{} 19 | -------------------------------------------------------------------------------- /examples/authentication/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars 2 | import { connect } from 'react-redux'; 3 | import { viewSelector } from './viewSelector'; 4 | 5 | import { Links } from './components/links'; 6 | 7 | export class App extends Component { 8 | render() { 9 | const { component } = this.props; 10 | return
11 | 12 | { component } 13 |
; 14 | } 15 | } 16 | 17 | @connect(viewSelector) 18 | export default class ConnectedApp extends App{} 19 | -------------------------------------------------------------------------------- /examples/authentication/containers/login.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars 2 | import { connect } from 'react-redux'; 3 | 4 | import { submitLogin } from '../actions/login'; 5 | 6 | export class Login extends Component { 7 | render() { 8 | return ( 9 |
10 |

Login page

11 | 12 |
13 | ); 14 | } 15 | } 16 | 17 | @connect(null, { submitLogin }) 18 | export default class ConnectedLogin extends Login {} 19 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './app'; 5 | import store from './store'; 6 | 7 | import * as routes from './routes'; 8 | import { connectToStore } from '../../index'; 9 | 10 | connectToStore(store, routes); 11 | 12 | class Root extends React.Component { 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | render(, document.getElementById('app-container')); 23 | -------------------------------------------------------------------------------- /examples/authentication/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './app'; 5 | import store from './store'; 6 | 7 | import * as routes from './routes'; 8 | import { connectToStore } from '../../index'; 9 | 10 | connectToStore(store, routes); 11 | 12 | class Root extends React.Component { 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | render(, document.getElementById('app-container')); 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [2, 2, {"SwitchCase": 1}], 4 | "quotes": [2, "single"], 5 | "linebreak-style": [2, "unix"], 6 | "semi": [2, "always"], 7 | "comma-dangle": [2, "always-multiline"], 8 | }, 9 | "env": { 10 | "es6": true, 11 | "browser": true, 12 | "node": true 13 | }, 14 | "extends": "eslint:recommended", 15 | "ecmaFeatures": { 16 | "destructuring": true, 17 | "arrowFunctions": true, 18 | "blockBindings": true, 19 | "classes": true, 20 | "jsx": true, 21 | "modules": true 22 | }, 23 | "parser": "babel-eslint", 24 | "plugins": [ 25 | "react" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic/viewSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { createComponentSelector, NO_MATCH } from '../../index'; 3 | 4 | import { 5 | home, 6 | a, 7 | b, 8 | bDeep, 9 | c, 10 | } from './routes'; 11 | 12 | import { 13 | Home, 14 | A, 15 | B, 16 | BDeep, 17 | C, 18 | } from './containers'; 19 | 20 | export const viewSelector = createComponentSelector({ 21 | [NO_MATCH]: () => , 22 | [home]: () => , 23 | [a]: () => , 24 | [b]: () => , 25 | [bDeep]: () => , 26 | [c]: (urlParams) => , 27 | }); 28 | -------------------------------------------------------------------------------- /examples/basic/components/links.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { Link } from '../../../index'; 3 | import { home, a, b, bDeep, c } from '../routes'; 4 | 5 | export function Links() { 6 | return ( 7 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/authentication/components/links.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { Link } from '../../../index'; 3 | import { me, user } from '../routes'; 4 | 5 | export function Links() { 6 | return ( 7 |
8 |
Open pages
9 | 13 |
Behind authentication
14 |
    15 |
  • Me (Link directive)
  • 16 |
  • Me
  • 17 |
  • W. White
  • 18 |
  • J. Pinkman
  • 19 |
  • Mr. Brown (link directive)
  • 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/minimalist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reroute-example-basic", 3 | "version": "0.0.0", 4 | "description": "Proper state-based routing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --content-base public/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ArnaudRinquin/reroute.git" 13 | }, 14 | "keywords": [ 15 | "router", 16 | "routing" 17 | ], 18 | "author": "Arnaud Rinquin", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/ArnaudRinquin/reroute/issues" 22 | }, 23 | "homepage": "https://github.com/ArnaudRinquin/reroute#readme", 24 | "devDependencies": { 25 | "babel-core": "^5.8.25", 26 | "babel-loader": "^5.3.2", 27 | "react": "^0.14.0", 28 | "react-dom": "^0.14.0", 29 | "react-redux": "^4.0.0", 30 | "redux": "^3.0.2", 31 | "webpack": "^1.12.2", 32 | "webpack-dev-server": "^1.12.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reroute-example-basic", 3 | "version": "0.0.0", 4 | "description": "Proper state-based routing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --content-base public/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ArnaudRinquin/reroute.git" 13 | }, 14 | "keywords": [ 15 | "router", 16 | "routing" 17 | ], 18 | "author": "Arnaud Rinquin", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/ArnaudRinquin/reroute/issues" 22 | }, 23 | "homepage": "https://github.com/ArnaudRinquin/reroute#readme", 24 | "devDependencies": { 25 | "babel-core": "^5.8.25", 26 | "babel-loader": "^5.3.2", 27 | "react": "^0.14.0", 28 | "react-dom": "^0.14.0", 29 | "react-redux": "^4.0.0", 30 | "redux": "^3.0.2", 31 | "reselect": "^2.0.0", 32 | "webpack": "^1.12.2", 33 | "webpack-dev-server": "^1.12.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reroute-example-authentication", 3 | "version": "0.0.0", 4 | "description": "Proper state-based routing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --content-base public/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ArnaudRinquin/reroute.git" 13 | }, 14 | "keywords": [ 15 | "router", 16 | "routing" 17 | ], 18 | "author": "Arnaud Rinquin", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/ArnaudRinquin/reroute/issues" 22 | }, 23 | "homepage": "https://github.com/ArnaudRinquin/reroute#readme", 24 | "devDependencies": { 25 | "babel-core": "^5.8.25", 26 | "babel-loader": "^5.3.2", 27 | "react": "^0.14.0", 28 | "react-dom": "^0.14.0", 29 | "react-redux": "^4.0.0", 30 | "redux": "^3.0.2", 31 | "reselect": "^2.0.0", 32 | "webpack": "^1.12.2", 33 | "webpack-dev-server": "^1.12.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/authentication/viewSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line no-unused-vars 2 | import { createSelector } from 'reselect'; 3 | import { createComponentSelector, noMatchRouteSelector } from '../../index'; 4 | 5 | import { home, users, me, user } from './routes'; 6 | import { Home, Users, Me, User, Login } from './containers'; 7 | 8 | const userSelector = ({user}) => user; 9 | const isAuthed = createSelector( 10 | userSelector, 11 | ({authorized}) => authorized 12 | ); 13 | 14 | export const viewSelector = createSelector( 15 | noMatchRouteSelector(), 16 | state => state, 17 | (noMatch, state) => { 18 | if (noMatch) { 19 | return { component: }; 20 | } 21 | return preAuthSelector(state) || auth(state); 22 | } 23 | ); 24 | 25 | const preAuthSelector = createComponentSelector({ 26 | [home]: () => , 27 | [users]: () => , 28 | }); 29 | 30 | const auth = createSelector( 31 | isAuthed, 32 | state => state, 33 | (isAuthed, state) => { 34 | return isAuthed ? postAuthSelector(state) : { component: }; 35 | } 36 | ); 37 | 38 | const postAuthSelector = createComponentSelector({ 39 | [me]: () => , 40 | [user]: (urlParams) => , 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-reroute", 3 | "description": "Location reducer and routing helpers for redux", 4 | "main": "index.jsx", 5 | "scripts": { 6 | "lint": "eslint index.jsx examples", 7 | "disabled-pretest": "npm run lint", 8 | "test": "mocha --compilers js:babel-core/register --recursive", 9 | "test:watch": "npm test -- --watch", 10 | "commit": "git-cz", 11 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/ArnaudRinquin/redux-reroute.git" 16 | }, 17 | "keywords": [ 18 | "reroute", 19 | "router", 20 | "routing", 21 | "redux" 22 | ], 23 | "author": "Arnaud Rinquin", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ArnaudRinquin/redux-reroute/issues" 27 | }, 28 | "homepage": "https://github.com/ArnaudRinquin/redux-reroute#readme", 29 | "dependencies": { 30 | "history": "^1.13.0", 31 | "url-pattern": "^1.0.1" 32 | }, 33 | "peerDependencies": { 34 | "react": ">=0.14.3" 35 | }, 36 | "devDependencies": { 37 | "babel": "^6.0.0", 38 | "babel-core": "^6.0.0", 39 | "babel-loader": "^6.0.0", 40 | "babel-preset-es2015": "^6.0.15", 41 | "babel-preset-react": "^6.0.15", 42 | "babel-preset-stage-0": "^6.0.15", 43 | "commitizen": "^2.4.0", 44 | "cz-conventional-changelog": "^1.1.4", 45 | "eslint": "^1.8.0", 46 | "eslint-plugin-react": "^3.6.3", 47 | "expect": "^1.13.4", 48 | "mocha": "^2.3.3", 49 | "semantic-release": "^4.3.5", 50 | "sinon": "^1.17.2" 51 | }, 52 | "czConfig": { 53 | "path": "./node_modules/cz-conventional-changelog" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /index.jsx: -------------------------------------------------------------------------------- 1 | import { createHistory } from 'history'; 2 | import UrlPattern from 'url-pattern'; 3 | import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars 4 | 5 | export const LOCATION_CHANGED = 'LOCATION_CHANGED'; 6 | export const NO_MATCH = 'NO_MATCH'; 7 | 8 | const noMatchPayload = { 9 | matchedRoute: NO_MATCH, 10 | urlParams: {}, 11 | }; 12 | 13 | export const extractFromHash = (location) => { 14 | if (!location.hash) { 15 | return '/'; 16 | } 17 | 18 | let path = location.hash; 19 | // remove # prefix 20 | path = path[0] === '#' ? path.slice(1) : path; 21 | // ensure / prefix 22 | path = path[0] === '/' ? path : `/${path}`; 23 | 24 | return path; 25 | }; 26 | 27 | export const pathToAction = (path, patterns) => { 28 | const payload = patterns.reduce((matching, pattern) => { 29 | const urlParams = pattern.match(path); 30 | if (urlParams) { 31 | return { 32 | matchedRoute: pattern.route, 33 | path, 34 | urlParams, 35 | }; 36 | } 37 | 38 | return matching; 39 | 40 | }, { ...noMatchPayload, path }); 41 | 42 | return { 43 | type: LOCATION_CHANGED, 44 | payload, 45 | }; 46 | }; 47 | 48 | export function generateLocationDispatcher(patterns, store, extractPath = extractFromHash) { 49 | return function dispatchLocation(location) { 50 | store.dispatch(pathToAction(extractPath(location), patterns)); 51 | }; 52 | } 53 | 54 | const isMatchable = (thing) => typeof thing === 'string' || thing instanceof RegExp; 55 | const toPattern = (route) => { 56 | const pattern = new UrlPattern(route); 57 | // unfortunately, UrlPattern does not 58 | // keep the original route so we do it manually 59 | pattern.route = route; 60 | return pattern; 61 | }; 62 | 63 | const toArray = (thing) => { 64 | if (Array.isArray(thing)) return thing; 65 | return Object.keys(thing).map((key) => thing[key]); 66 | }; 67 | 68 | export function generatePatterns(routes) { 69 | return toArray(routes) 70 | .filter(isMatchable) 71 | .map(toPattern); 72 | } 73 | 74 | export function connectToStore(store, routes) { 75 | const history = createHistory(); 76 | const patterns = generatePatterns(routes); 77 | const locationDispatcher = generateLocationDispatcher(patterns, store); 78 | return history.listen(locationDispatcher); 79 | } 80 | 81 | export function location(state = noMatchPayload, {type, payload}) { 82 | if (type === LOCATION_CHANGED) { 83 | return { 84 | ...state, 85 | ...payload, 86 | }; 87 | } 88 | 89 | return state; 90 | } 91 | 92 | export function Link({route, urlParams, children}) { 93 | const pattern = new UrlPattern(route); 94 | return {children}; 95 | } 96 | 97 | Link.propTypes = { 98 | route: PropTypes.string.isRequired, 99 | urlParams: PropTypes.object, 100 | children: PropTypes.node, 101 | }; 102 | 103 | function defaultLocationSelector({location}) { 104 | return location; 105 | } 106 | 107 | export function noMatchRouteSelector(locationSelector = defaultLocationSelector) { 108 | return function(state) { 109 | const { matchedRoute } = locationSelector(state); 110 | return matchedRoute === NO_MATCH ? NO_MATCH : null; 111 | }; 112 | } 113 | 114 | export function createComponentSelector(routeToComponentCreators, locationSelector = defaultLocationSelector) { 115 | const routes = Object.keys(routeToComponentCreators); 116 | return function componentSelector(state) { 117 | 118 | const { matchedRoute, urlParams } = locationSelector(state); 119 | 120 | const componentCreator = routes.reduce((componentCreator, route) => { 121 | if (matchedRoute === route) { 122 | return routeToComponentCreators[route]; 123 | } 124 | return componentCreator; 125 | }, null); 126 | 127 | if (componentCreator) { 128 | const component = componentCreator(urlParams); 129 | return { component }; 130 | } 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /examples/minimalist/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; // eslint-disable-line no-unused-vars 2 | import { render } from 'react-dom'; 3 | import { Provider, connect } from 'react-redux'; 4 | import { createStore, combineReducers } from 'redux'; 5 | 6 | import { connectToStore, location } from '../../index'; 7 | 8 | // Regular example actions and reducers 9 | const increment = (by = 1) => ({type: 'INCREMENT', by}); 10 | const decrement = (by = 1) => ({type: 'DECREMENT', by}); 11 | 12 | const counter = (state = 0, {type, by}) => { 13 | switch (type) { 14 | case 'INCREMENT': 15 | return state + by; 16 | case 'DECREMENT': 17 | return state - by; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | // redux reducers and store creation 24 | const rootReducer = combineReducers({ 25 | location, // the `reroute` location reducer 26 | counter, // your own reducers 27 | }); 28 | 29 | const store = createStore(rootReducer); 30 | 31 | // Define your route templates 32 | // the object keys aren't used internally, they are just for latter reference 33 | // `connectToStore` actually transforms it to an array of string. 34 | const routes = { 35 | home: '/', 36 | buttons: '/path/to/buttons(/by/:by)', 37 | total: '/path/to/total', 38 | }; 39 | 40 | // Connect the `reroute` location handler to the store 41 | connectToStore(store, routes); 42 | 43 | // Regular application top level components connect to redux 44 | // Only to get access to `actions` and bits of the app state 45 | @connect((state) => ({by: state.location.urlParams.by}), { increment, decrement }) 46 | class ButtonsPage extends Component { 47 | render() { 48 | const by = parseInt(this.props.by || 1); 49 | return ( 50 |
51 | 52 | 53 |
By {by}
54 |
55 | ); 56 | } 57 | } 58 | 59 | @connect(({counter}) => ({counter})) 60 | class TotalPage extends Component { 61 | render() { 62 | const { counter } = this.props; 63 | return ( 64 |
65 |

Total: {counter}

66 |
67 | ); 68 | } 69 | } 70 | 71 | // This component is responsible for picking and rendering the right component 72 | // It connects to get the `location.matchedRoute` value of the app state 73 | // 74 | // Note: this is not necessary when using a component selector, this is only 75 | // meant to demonstrate the `reroute` principles. 76 | @connect(state => ({ matchedRoute: state.location.matchedRoute })) 77 | class ComponentSwitch extends Component { 78 | render() { 79 | const {matchedRoute} = this.props; 80 | 81 | // Simply render the right component based on `matchedRoute` 82 | switch (matchedRoute) { 83 | case routes.home: return
Home page
; 84 | case routes.buttons: return ; 85 | case routes.total: return ; 86 | default: return
unknown route
; 87 | } 88 | } 89 | } 90 | 91 | // The top level component, just a list of links and the component switch 92 | // Note how we only use regular `` elements for navigation 93 | class App extends Component { 94 | render() { 95 | return ( 96 | 97 | 108 | 109 | ); 110 | } 111 | } 112 | 113 | render(, document.getElementById('app-container')); 114 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { spy } from 'sinon'; 3 | import { 4 | LOCATION_CHANGED, 5 | NO_MATCH, 6 | generatePatterns, 7 | extractFromHash, 8 | pathToAction, 9 | generateLocationDispatcher, 10 | } from '../index.jsx'; 11 | import UrlPattern from 'url-pattern'; 12 | 13 | describe('generatePatterns', () => { 14 | const expectToBeUrlPattern = (pattern) => expect(pattern).toBeA(UrlPattern); 15 | const legitRoutes = [ 16 | '/route/a', 17 | '/route/b', 18 | '/route/b/but/deeper', 19 | /reg\/route/, 20 | ]; 21 | 22 | context('when given an array', () => { 23 | 24 | it('should return an array of UrlPatterns', () => { 25 | const patterns = generatePatterns(legitRoutes); 26 | expect(Array.isArray(patterns)).toBe(true, 'should return an array'); 27 | expect(patterns.length).toBeGreaterThan(0, 'should return some values'); 28 | patterns.forEach(expectToBeUrlPattern); 29 | }); 30 | 31 | it('should strip out non string or non regexp values', () => { 32 | const routes = [ 33 | ...legitRoutes, 34 | true, 35 | [], 36 | {obj: 'is an object'}, 37 | ]; 38 | const patterns = generatePatterns(routes); 39 | expect(patterns.length).toBe(legitRoutes.length); 40 | }); 41 | 42 | it('should attach original route to UrlPattern', () => { 43 | const patterns = generatePatterns(legitRoutes); 44 | patterns.forEach((pattern, index) => expect(pattern.route).toBe(legitRoutes[index])); 45 | }); 46 | }); 47 | 48 | context('when given an Object', () => { 49 | const routes = [ 50 | '/home', 51 | '/users/:userId', 52 | '/users', 53 | ]; 54 | 55 | const routeObject = { 56 | home: routes[0], 57 | user: routes[1], 58 | users: routes[2], 59 | }; 60 | 61 | it('should use the object values as input', () => { 62 | const patternsFromObject = generatePatterns(routeObject); 63 | const patternsFromArray = generatePatterns(routes); 64 | expect(patternsFromObject).toEqual(patternsFromArray); 65 | }); 66 | }); 67 | }); 68 | 69 | describe('extractFromHash(location)', () => { 70 | it('should return location.hash', function(){ 71 | expect(extractFromHash({hash: '/foo'})).toEqual('/foo'); 72 | }); 73 | 74 | it('should remove "#" prefix', function(){ 75 | expect(extractFromHash({hash: '#/foo'})).toEqual('/foo'); 76 | }); 77 | 78 | it('should ensure a "/" prefix', function(){ 79 | expect(extractFromHash({hash: '#foo'})).toEqual('/foo'); 80 | expect(extractFromHash({hash: 'foo'})).toEqual('/foo'); 81 | }); 82 | 83 | it('should return "/" when hash is falsy', () =>{ 84 | expect(extractFromHash({hash: null})).toEqual('/'); 85 | expect(extractFromHash({hash: undefined})).toEqual('/'); 86 | expect(extractFromHash({hash: false})).toEqual('/'); 87 | }); 88 | 89 | it('should return "/" when hash is empty string', () => { 90 | expect(extractFromHash({hash: ''})).toEqual('/'); 91 | }); 92 | }); 93 | 94 | function matchingPattern(route, urlParams) { 95 | return { 96 | route, 97 | match: () => urlParams, 98 | }; 99 | } 100 | 101 | function nonMatchingPattern(route) { 102 | return { 103 | route, 104 | match: () => null, 105 | }; 106 | } 107 | 108 | describe('pathToAction', () => { 109 | 110 | it('returns a LOCATION_CHANGED type action with matchedRoute and urlParams as payload', () => { 111 | const patterns = [ 112 | matchingPattern('matching', {foo:'bar'}), 113 | nonMatchingPattern('non-matching'), 114 | ]; 115 | 116 | const action = pathToAction('whatever', patterns); 117 | const {type, payload} = action; 118 | const {matchedRoute, urlParams} = payload; 119 | 120 | expect(type).toBe(LOCATION_CHANGED); 121 | expect(matchedRoute).toBe('matching'); 122 | expect(urlParams).toEqual({foo:'bar'}); 123 | }); 124 | 125 | it('returns an LOCATION_CHANGED action with the NO_MATCH and empty object as payload', () => { 126 | const action = pathToAction('whatever', []); 127 | const {type, payload} = action; 128 | const {matchedRoute, urlParams} = payload; 129 | 130 | expect(type).toBe(LOCATION_CHANGED); 131 | expect(matchedRoute).toBe(NO_MATCH); 132 | expect(urlParams).toEqual({}); 133 | }); 134 | 135 | context('when many routes match', () => { 136 | it('returns an aciton out out the last matching pattern', () => { 137 | const patterns = [ 138 | matchingPattern('matching', {foo:'bar'}), 139 | matchingPattern('matching', {foo:'bar2'}), 140 | matchingPattern('matching', {foo:'bar3'}), 141 | matchingPattern('theLastOneMatching', {isThe:'oneWeWant'}), 142 | ]; 143 | 144 | const action = pathToAction('whatever', patterns); 145 | const {type, payload} = action; 146 | const {matchedRoute, urlParams} = payload; 147 | 148 | expect(type).toBe(LOCATION_CHANGED); 149 | expect(matchedRoute).toBe('theLastOneMatching'); 150 | expect(urlParams).toEqual({isThe:'oneWeWant'}); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('generateLocationDispatcher(patterns, store, extractPath)', () => { 156 | it('should return a dispatchLocation function', () => { 157 | expect(typeof generateLocationDispatcher()).toBe('function'); 158 | }); 159 | 160 | describe('returned function', function (){ 161 | beforeEach(() => { 162 | this.store = { 163 | dispatch(){}, 164 | }; 165 | this.dispatchSpy = spy(this.store, 'dispatch'); 166 | 167 | this.patterns = [ 168 | matchingPattern('matching', {}), 169 | nonMatchingPattern('non-matching'), 170 | matchingPattern('lastMatching', {foo:'bar'}), 171 | ]; 172 | 173 | this.extractPath = () => 'extractedPath'; 174 | 175 | this.dispatchLocation = generateLocationDispatcher(this.patterns, this.store, this.extractPath); 176 | }); 177 | 178 | it('should dispatch an action', () => { 179 | this.dispatchLocation('location'); 180 | expect(this.dispatchSpy.callCount).toBe(1); 181 | }); 182 | 183 | describe('disaptchedAction', () => { 184 | beforeEach(() => { 185 | this.dispatchLocation('location'); 186 | this.action = this.dispatchSpy.args[0][0]; 187 | }); 188 | 189 | it('should be of type LOCATION_CHANGED', () => { 190 | expect(this.action.type).toBe(LOCATION_CHANGED); 191 | }); 192 | 193 | it('should have matched pattern as `matchedRoute` in payload', () => { 194 | const {payload: {matchedRoute}} = this.action; 195 | expect(matchedRoute).toBe('lastMatching'); 196 | }); 197 | 198 | it('should have matched pattern result as `urlParams` in payload', () => { 199 | const {payload: {urlParams}} = this.action; 200 | expect(urlParams).toEqual({foo:'bar'}); 201 | }); 202 | 203 | it('should have extracted path as `urlParams` in payload', () => { 204 | const {payload: {path}} = this.action; 205 | expect(path).toEqual('extractedPath'); 206 | }); 207 | }); 208 | 209 | context('when called without extraction path', () => { 210 | beforeEach(() => { 211 | this.dispatchLocation = generateLocationDispatcher(this.patterns, this.store); 212 | }); 213 | 214 | it('should extract route from hash by default', () => { 215 | this.dispatchLocation({hash: '#/route/to/anything'}); 216 | const action = this.dispatchSpy.args[0][0]; 217 | expect(action.payload.path).toEqual('/route/to/anything'); 218 | }); 219 | }); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-reroute 2 | 3 | [![Build Status](https://travis-ci.org/ArnaudRinquin/redux-reroute.svg)](https://travis-ci.org/ArnaudRinquin/redux-reroute) 4 | [![Dependency Status](https://david-dm.org/ArnaudRinquin/redux-reroute.svg)](https://david-dm.org/ArnaudRinquin/redux-reroute) 5 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 6 | 7 | Location reducer and routing helpers for [`redux`](https://github.com/rackt/redux). 8 | 9 | ## Rationale 10 | 11 | **Context** We are using an application state based rendering flow (redux) 12 | 13 | **Questions** 14 | * How to deal with the location changes? 15 | * Should the URL be the result of the app state? - Or - 16 | * Should the application state be the result of the URL? 17 | * Where do I put my non-location based routing logic (ex: authentication)? 18 | * How to implement deep linking? 19 | 20 | **Choices made by `redux-reroute`** 21 | 22 | * The application state remains the only source of truth 23 | * The location, or url, is only a part of the application state 24 | * It is up to the developer to build the decision tree resulting in the intended UI 25 | * `redux-reroute` = minimal base + optional helpers 26 | * Based on idiomatic `redux` principles (action > reducer > app state > render loop) 27 | * There is not concept of `ViewContainer`, `Router` or `Route` components 28 | * Determine which component to render by using a component `selector` (`(appState) => Component`, as defined by [`reselect`](https://github.com/rackt/reselect)), anywhere in your application 29 | 30 | ## Features 31 | 32 | 1. reduce the location into the application state, providing: 33 | 1. matched url pattern 34 | 1. url parameters (within the path or query string) 35 | 1. optionally use provided helpers to generate your component `selector(s)` (as defined by [`reselect`](https://github.com/rackt/reselect)) 36 | 37 | ## The minimal example ([source](./examples/minimalist/index.js)) 38 | 39 | In this example, we demonstrate the base principles: 40 | 41 | * declare some route patterns (using [`url-pattern`](https://github.com/snd/url-pattern) syntaxe) 42 | * connect `reroute` to the store 43 | * decide which component to show depending on app state 44 | * pick url params from app state 45 | * navigate using regular links 46 | 47 | Note: helpers provided by `reroute` remove the need for most of the boilerplate shown in this example. 48 | 49 | ```js 50 | import React, { Component } from 'react'; 51 | import { render } from 'react-dom'; 52 | import { Provider, connect } from 'react-redux'; 53 | import { createStore, combineReducers } from 'redux'; 54 | 55 | import { connectToStore, location } from 'redux-reroute'; 56 | 57 | // Regular example actions and reducers 58 | const increment = (by = 1) => ({type: 'INCREMENT', by}); 59 | const decrement = (by = 1) => ({type: 'DECREMENT', by}); 60 | 61 | const counter = (state = 0, {type, by}) => { 62 | switch (type) { 63 | case 'INCREMENT': 64 | return state + by; 65 | case 'DECREMENT': 66 | return state - by; 67 | default: 68 | return state; 69 | } 70 | } 71 | 72 | // redux reducers and store creation 73 | const rootReducer = combineReducers({ 74 | location, // the `reroute` location reducer 75 | counter // your own reducers 76 | }); 77 | 78 | const store = createStore(rootReducer); 79 | 80 | // Define your route templates 81 | // the object keys aren't used internally, they are just for latter reference 82 | // `connectToStore` actually transforms it to an array of string. 83 | const routes = { 84 | home: '/', 85 | buttons: '/path/to/buttons(/by/:by)', 86 | total: '/path/to/total' 87 | }; 88 | 89 | // Connect the `reroute` location handler to the store 90 | const disconnect = connectToStore(store, routes); 91 | 92 | // Regular application top level components connect to redux 93 | // Only to get access to `actions` and bits of the app state 94 | @connect((state) => ({by: state.location.urlParams.by}), { increment, decrement }) 95 | class ButtonsPage extends Component { 96 | render() { 97 | const by = parseInt(this.props.by || 1); 98 | return ( 99 |
100 | 101 | 102 |
By {by}
103 |
104 | ); 105 | } 106 | } 107 | 108 | @connect(({counter}) => ({counter})) 109 | class TotalPage extends Component { 110 | render() { 111 | const { counter } = this.props; 112 | return ( 113 |
114 |

Total: {counter}

115 |
116 | ); 117 | } 118 | } 119 | 120 | // This component is responsible for picking and rendering the right component 121 | // It connects to get the `location.matchedRoute` value of the app state 122 | // 123 | // Note: this is not necessary when using a component selector, this is only 124 | // meant to demonstrate the `reroute` principles. 125 | @connect(state => ({ matchedRoute: state.location.matchedRoute })) 126 | class ComponentSwitch extends Component { 127 | render() { 128 | const {matchedRoute} = this.props; 129 | 130 | // Simply render the right component based on `matchedRoute` 131 | switch (matchedRoute) { 132 | case routes.home: return
Home page
133 | case routes.buttons: return 134 | case routes.total: return 135 | default: return
unknown route
136 | } 137 | } 138 | } 139 | 140 | // The top level component, just a list of links and the component switch 141 | // Note how we only use regular `` elements for navigation 142 | class App extends Component { 143 | render() { 144 | return ( 145 | 146 | 157 | 158 | ); 159 | } 160 | } 161 | 162 | render(, document.getElementById('app-container')); 163 | ``` 164 | 165 | ## API 166 | 167 | No comprehensive API doc for now, have a look at the examples. 168 | 169 | However, here are the bits of code provided by `reroute`: 170 | 171 | * `connectToStore(store, routes)`, must-call, to dispatch location related actions 172 | * `location` reducer, filling the app state with `matchedRoute` and `urlParams` 173 | * `Link` component to generate links from route patterns and url parameters 174 | * `createComponentSelector` helper to create component selector 175 | * `noMatchRouteSelector` helper to generate a selector returning whether a route is matched 176 | 177 | ### `createComponentSelector(routeToComponentCreators, [locationSelector])` 178 | 179 | This helper is meant to ease route-pattern mapping to components. 180 | 181 | ```js 182 | // [usual dependencies] 183 | import { createComponentSelector, connectToStore, NO_MATCH } from `reroute`; 184 | 185 | // 186 | const routes = { 187 | home: '/', 188 | users: '/users', 189 | user: '/users/:userId' 190 | }; 191 | 192 | connectToStore(store, routes); 193 | 194 | // Create a component selector that will use `matchedRoute` to 195 | // pick the right component 196 | const componentSelector = createComponentSelector({ 197 | [routes.home]: () => , 198 | [routes.users]: () => , 199 | [routes.user]: (urlParams) => , 200 | [NO_MATCH]: () => 201 | }); 202 | 203 | // Connect your component using create componentSelector 204 | @connect(componentSelector) 205 | class App extends React.Component { 206 | render() { 207 | // `this.props.component` is the component you should render 208 | const { component } = this.props; 209 | return
{component}
210 | } 211 | } 212 | ``` 213 | 214 | If the logic of your app routing is more complex than just mapping URL 215 | to component (like authentication), you should still use this helper to 216 | create component selectors and combine them using [`reselect`](https://github.com/rackt/reselect) as in [this 217 | example](./examples/authentication/). 218 | 219 | ## Recipes / Examples 220 | 221 | * [more generic route to component routing](./examples/basic/) 222 | * [protect some views behing authentication](./examples/authentication/) 223 | 224 | ### Moving `location` data within the application state tree 225 | 226 | In most case, the `location` reducer will be used right in the root reducer so 227 | the `location` data is there, at the root. 228 | 229 | However, you might want to move it somewhere else. _No problem_. 230 | 231 | The only side effect is that the various helpers won't find the `location` data where they expect it to be. All you need to do is to pass them the optional `location` selector. 232 | 233 | ```js 234 | 235 | import { location } from 'redux-reroute'; 236 | import { combineReducer } from 'redux'; 237 | import { myReducer } from './reducers/mine'; 238 | import { myOtherReducer } from './reducers/other'; 239 | 240 | // manually building an `stuff` reducer containing the `location` data under `path` 241 | const stuff = (state = {}, action) { 242 | return { 243 | path: location(state.path, action) 244 | }; 245 | } 246 | 247 | const rootReducer = combineReducer({ 248 | myReducer, 249 | myOtherReducer, 250 | stuff 251 | }); 252 | 253 | // in this context, the `location` data will accessible this way: 254 | // location = store.getState().stuff.path 255 | 256 | // Just create the location selector pointing to the right place 257 | const locationSelector = (state) => state.stuff.path 258 | 259 | // Example for `createComponentSelector` 260 | const componentSelector = createComponentSelector({ 261 | // your regular routePattern > component settings 262 | [routes.home]: 263 | [routes.users]: 264 | }, locationSelector); // <- the last option parameter is the locationSelector 265 | ``` 266 | 267 | ## TODO 268 | 269 | * More examples 270 | * `#`-free path handling 271 | * asynchronous location change, allowing things like loading data 272 | * tests, once the API is a little bit stabilized 273 | 274 | ## Why not `redux-router`? 275 | 276 | We found out than the routing provided by `redux-router` does not let you integrate non-location based routing easily. 277 | 278 | You will have to work against it to inject your non-location routing logic, by using either: 279 | 280 | * more routing logic in the component `Router` outputs, splitting the routing logic into location vs. non-location code. 281 | * redirecting using `onEnter`, creating some weird non-idiomatic loops. 282 | * re-wrapped and replace the reducer `react-redux` uses 283 | * ??? 284 | 285 | With `reroute`, you don't have to work around anythings. You build the routing by composing location and non-location related application state. 286 | 287 | ![reroute-vs-redux-router](./images/reroute-vs-redux-router.png) 288 | --------------------------------------------------------------------------------