├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── examples ├── .babelrc ├── README.md ├── basic │ ├── index.html │ ├── index.js │ ├── package.json │ ├── server.js │ └── webpack.config.clientDev.js ├── mergeConfig.js ├── package.json ├── server-rendering │ ├── client.js │ ├── components.js │ ├── constants.js │ ├── package.json │ ├── reducer.js │ ├── routes.js │ ├── server.js │ └── webpack.config.clientDev.js └── webpack.config.base.js ├── package.json └── src ├── ReduxRouter.js ├── __tests__ ├── ReduxRouter-test.js ├── init.js └── reduxReactRouter-test.js ├── actionCreators.js ├── client.js ├── constants.js ├── historyMiddleware.js ├── index.js ├── isActive.js ├── matchMiddleware.js ├── reduxReactRouter.js ├── replaceRoutesMiddleware.js ├── routeReplacement.js ├── routerStateEquals.js ├── routerStateReducer.js ├── server.js ├── serverModule.js └── useDefaults.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "globals": { 9 | "expect": true 10 | }, 11 | "rules": { 12 | "comma-dangle": 0, 13 | "no-wrap-func": 0, 14 | "spaced-comment": 0, 15 | 16 | "no-undef": 2, 17 | 18 | // Doesn't play nice with chai's assertions 19 | "no-unused-expressions": 0, 20 | 21 | // Discourages microcomponentization 22 | "react/no-multi-comp": 0, 23 | 24 | "react/jsx-uses-react": 2, 25 | "react/jsx-uses-vars": 2, 26 | "react/react-in-jsx-scope": 2, 27 | 28 | //Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) 29 | "block-scoped-var": 0, 30 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved 31 | "padded-blocks": 0 32 | }, 33 | "plugins": [ 34 | "react" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | lib 5 | /server.js 6 | dist 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - 4 5 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | ## Version 25 | 26 | ## Steps to reproduce 27 | 28 | ## Expected Behavior 29 | 30 | ## Actual Behavior 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Andrew Clark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redux-router 2 | ============ 3 | 4 | [![build status](https://img.shields.io/travis/acdlite/redux-router/master.svg?style=flat-square)](https://travis-ci.org/acdlite/redux-router) 5 | [![npm version](https://img.shields.io/npm/v/redux-router.svg?style=flat-square)](https://www.npmjs.com/package/redux-router) 6 | [![redux-router on discord](https://img.shields.io/badge/discord-redux--router@reactiflux-738bd7.svg?style=flat-square)](https://discord.gg/0ZcbPKXt5bVkq8Eo) 7 | 8 | ## This project is experimental. 9 | 10 | ### For bindings for React Router 1.x see [here](https://github.com/acdlite/redux-router/tree/263f623f6e8a8ec165568216888e572a48c5cd54) 11 | 12 | ### In most cases, you don’t need any library to bridge Redux and React Router. Just use React Router directly. 13 | 14 | ## Please check out [the differences between react-router-redux and redux-router](#differences-with-react-router-redux) before using this library 15 | 16 | [Redux](redux.js.org) bindings for [React Router](https://github.com/reactjs/react-router). 17 | 18 | - Keep your router state inside your Redux Store. 19 | - Interact with the Router with the same API you use to interact with the rest of your app state. 20 | - Completely interoperable with existing React Router API. ``, `router.transitionTo()`, etc. still work. 21 | - Serialize and deserialize router state. 22 | - Works with time travel feature of Redux Devtools! 23 | 24 | ```sh 25 | # installs version 2.x.x 26 | npm install --save redux-router 27 | ``` 28 | Install the version 1.0.0 via the `previous` tag 29 | ```sh 30 | npm install --save redux-router@previous 31 | ``` 32 | 33 | 34 | ## Why 35 | 36 | React Router is a fantastic routing library, but one downside is that it abstracts away a very crucial piece of application state — the current route! This abstraction is super useful for route matching and rendering, but the API for interacting with the router to 1) trigger transitions and 2) react to state changes within the component lifecycle leaves something to be desired. 37 | 38 | It turns out we already solved these problems with Flux (and Redux): We use action creators to trigger state changes, and we use higher-order components to subscribe to state changes. 39 | 40 | This library allows you to keep your router state **inside your Redux store**. So getting the current pathname, query, and params is as easy as selecting any other part of your application state. 41 | 42 | ## Example 43 | 44 | ```js 45 | import React from 'react'; 46 | import { combineReducers, applyMiddleware, compose, createStore } from 'redux'; 47 | import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router'; 48 | import { createHistory } from 'history'; 49 | import { Route } from 'react-router'; 50 | 51 | // Configure routes like normal 52 | const routes = ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | 61 | // Configure reducer to store state at state.router 62 | // You can store it elsewhere by specifying a custom `routerStateSelector` 63 | // in the store enhancer below 64 | const reducer = combineReducers({ 65 | router: routerStateReducer, 66 | //app: rootReducer, //you can combine all your other reducers under a single namespace like so 67 | }); 68 | 69 | // Compose reduxReactRouter with other store enhancers 70 | const store = compose( 71 | applyMiddleware(m1, m2, m3), 72 | reduxReactRouter({ 73 | routes, 74 | createHistory 75 | }), 76 | devTools() 77 | )(createStore)(reducer); 78 | 79 | 80 | // Elsewhere, in a component module... 81 | import { connect } from 'react-redux'; 82 | import { push } from 'redux-router'; 83 | 84 | connect( 85 | // Use a selector to subscribe to state 86 | state => ({ q: state.router.location.query.q }), 87 | 88 | // Use an action creator for navigation 89 | { push } 90 | )(SearchBox); 91 | ``` 92 | 93 | You will find a **server-rendering** example in the repo´s example directory. 94 | 95 | ### Works with Redux Devtools (and other external state changes) 96 | 97 | redux-router will notice if the router state in your Redux store changes from an external source other than the router itself — e.g. the Redux Devtools — and trigger a transition accordingly! 98 | 99 | ## Differences with react-router-redux 100 | 101 | #### react-router-redux 102 | 103 | [react-router-redux](https://github.com/reactjs/react-router-redux) (formerly redux-simple-router) takes a different approach to 104 | integrating routing with redux. react-router-redux lets React Router do all the heavy lifting and syncs the url data to a history 105 | [location](https://github.com/mjackson/history/blob/master/docs/Location.md#location) object in the store. This means that users can use 106 | React Router's APIs directly and benefit from the wide array of documentation and examples there. 107 | 108 | The README for react-router-redux has a useful picture included here: 109 | 110 | [redux](https://github.com/reactjs/redux) (`store.routing`)  ↔  [**react-router-redux**](https://github.com/reactjs/react-router-redux)  ↔  [history](https://github.com/reactjs/history) (`history.location`)  ↔  [react-router](https://github.com/reactjs/react-router) 111 | 112 | This approach, while simple to use, comes with a few caveats: 113 | 1. The history location object does not include React Router params and they must be either passed down from a React Router component or recomputed. 114 | 2. It is discouraged (and dangerous) to connect the store data to a component because the store data potentially updates **after** the React Router properties have changed, therefore there can be race conditions where the location store data differs from the location object passed down via React Router to components. 115 | 116 | react-router-redux encourages users to use props directly from React Router in the components (they are passed down to any rendered route components). This means that if you want to access the location data far down the component tree, you may need to pass it down or use React's context feature. 117 | 118 | #### redux-router 119 | 120 | This project, on the other hand takes the approach of storing the **entire** React Router data inside the redux store. This has the main benefit that it becomes impossible for the properties passed down by redux-router to the components in the Route to differ from the data included in the store. Therefore feel free to connect the router data to any component you wish. You can also access the route params from the store directly. redux-router also provides an API for hot swapping the routes from the Router (something React Router does not currently provide). 121 | 122 | The picture of redux-router would look more like this: 123 | 124 | [redux](https://github.com/reactjs/redux) (`store.router`)  ↔  [**redux-router**](https://github.com/acdlite/redux-router)  ↔  [react-router (via RouterContext)](https://github.com/reactjs/react-router) 125 | 126 | This approach, also has its set of limitations: 127 | 1. The router data is not all serializable (because Components and functions are not direclty serializable) and therefore this can cause issues with some devTools extensions and libraries that help in saving the store to the browser session. This can be mitigated if the libraries offer ways to ignore seriliazing parts of the store but is not always possible. 128 | 2. redux-router takes advantage of the RouterContext to still use much of React Router's internal logic. However, redux-router must still implement many things that React Router already does on its own and can cause delays in upgrade paths. 129 | 3. redux-router must provide a slightly different top level API (due to 2) even if the Route logic/matching is identical 130 | 131 | 132 | Ultimately, your choice in the library is up to you and your project's needs. react-router-redux will continue to have a larger support 133 | in the community due to its inclusion into the reactjs github organization and visibility. **react-router-redux is the recommended approach** 134 | for react-router and redux integration. However, you may find that redux-router aligns better with your project's needs. 135 | redux-router will continue to be mantained as long as demand exists. 136 | 137 | ## API 138 | 139 | ### `reduxReactRouter({ routes, createHistory, routerStateSelector })` 140 | 141 | A Redux store enhancer that adds router state to the store. 142 | 143 | ### `routerStateReducer(state, action)` 144 | 145 | A reducer that keeps track of Router state. 146 | 147 | ### `` 148 | 149 | A component that renders a React Router app using router state from a Redux store. 150 | 151 | ### `push(location)` 152 | 153 | An action creator for `history.push()`. [mjackson/history/docs/GettingStarted.md#navigation](https://github.com/mjackson/history/blob/master/docs/GettingStarted.md#navigation) 154 | 155 | Basic example (let say we are at `http://example.com/orders/new`): 156 | ```js 157 | dispatch(push('/orders/' + order.id)); 158 | ``` 159 | Provided that `order.id` is set and equals `123` it will change browser address bar to `http://example.com/orders/123` and appends this URL to the browser history (without reloading the page). 160 | 161 | A bit more advanced example: 162 | ```js 163 | dispatch(push({ 164 | pathname: '/orders', 165 | query: { filter: 'shipping' } 166 | })); 167 | ``` 168 | This will change the browser address bar to `http://example.com/orders?filter=shipping`. 169 | 170 | **NOTE:** clicking back button will change address bar back to `http://example.com/orders/new` but will **not** change page content 171 | 172 | ### `replace(location)` 173 | 174 | An action creator for `history.replace()`. [mjackson/history/docs/GettingStarted.md#navigation](https://github.com/mjackson/history/blob/master/docs/GettingStarted.md#navigation) 175 | 176 | Works similar to the `push` except that it doesn't create new browser history entry. 177 | 178 | **NOTE:** Referring to the `push` example: clicking back button will change address bar back to the URL before `http://example.com/orders/new` and will change page content. 179 | 180 | ### `go(n)` `goBack()` `goForward()` 181 | 182 | ```js 183 | // Go back to the previous entry in browser history. 184 | // These lines are synonymous. 185 | history.go(-1); 186 | history.goBack(); 187 | 188 | // Go forward to the next entry in browser history. 189 | // These lines are synonymous. 190 | history.go(1); 191 | history.goForward(); 192 | ``` 193 | 194 | ## Handling authentication via a higher order component 195 | 196 | @joshgeller threw together a good example on how to handle user authentication via a higher order component. Check out [joshgeller/react-redux-jwt-auth-example](https://github.com/joshgeller/react-redux-jwt-auth-example) 197 | 198 | ## Bonus: Reacting to state changes with redux-rx 199 | 200 | This library pairs well with [redux-rx](https://github.com/acdlite/redux-rx) to trigger route transitions in response to state changes. Here's a simple example of redirecting to a new page after a successful login: 201 | 202 | ```js 203 | const LoginPage = createConnector(props$, state$, dispatch$, () => { 204 | const actionCreators$ = bindActionCreators(actionCreators, dispatch$); 205 | const push$ = actionCreators$.map(ac => ac.push); 206 | 207 | // Detect logins 208 | const didLogin$ = state$ 209 | .distinctUntilChanged(state => state.loggedIn) 210 | .filter(state => state.loggedIn); 211 | 212 | // Redirect on login! 213 | const redirect$ = didLogin$ 214 | .withLatestFrom( 215 | push$, 216 | // Use query parameter as redirect path 217 | (state, push) => () => push(state.router.query.redirect || '/') 218 | ) 219 | .do(go => go()); 220 | 221 | return combineLatest( 222 | props$, actionCreators$, redirect$, 223 | (props, actionCreators) => ({ 224 | ...props, 225 | ...actionCreators 226 | }); 227 | }); 228 | ``` 229 | 230 | A more complete example is forthcoming. 231 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Redux-Router Examples 2 | ====== 3 | 4 | To run the examples in your development environment: 5 | 6 | 1. Run `npm install` from the root directory (redux-router) 7 | 2. Run `npm install` from the examples directory (this directory) 8 | 3. Run `npm start` from the example you wish to start (i.e. basic or server-rendering) 9 | 4. Point your browser to http://localhost:3000 10 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redux React Router – Basic Example 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/basic/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, compose, combineReducers } from 'redux'; 4 | 5 | import { 6 | ReduxRouter, 7 | routerStateReducer, 8 | reduxReactRouter, 9 | push, 10 | } from 'redux-router'; 11 | 12 | import { Route, Link } from 'react-router'; 13 | import { Provider, connect } from 'react-redux'; 14 | import { devTools } from 'redux-devtools'; 15 | import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react'; 16 | import { createHistory } from 'history'; 17 | 18 | @connect((state) => ({})) 19 | class App extends Component { 20 | static propTypes = { 21 | children: PropTypes.node 22 | } 23 | 24 | constructor(props) { 25 | super(props); 26 | this.handleClick = this.handleClick.bind(this); 27 | } 28 | 29 | handleClick(event) { 30 | event.preventDefault(); 31 | const { dispatch } = this.props; 32 | 33 | dispatch(push({ pathname: '/parent/child/custom' })); 34 | } 35 | 36 | render() { 37 | // Display is only used for rendering, its not a property of 38 | const links = [ 39 | { pathname: '/', display: '/'}, 40 | { pathname: '/parent', query: { foo: 'bar' }, display: '/parent?foo=bar'}, 41 | { pathname: '/parent/child', query: { bar: 'baz' }, display: '/parent/child?bar=baz'}, 42 | { pathname: '/parent/child/123', query: { baz: 'foo' }, display: '/parent/child/123?baz=foo'} 43 | ].map((l, i) => 44 |

45 | {l.display} 46 |

47 | ); 48 | 49 | return ( 50 |
51 |

App Container

52 | {links} 53 | 54 | 55 | /parent/child/custom 56 | 57 | {this.props.children} 58 |
59 | ); 60 | } 61 | } 62 | 63 | class Parent extends Component { 64 | static propTypes = { 65 | children: PropTypes.node 66 | } 67 | 68 | render() { 69 | return ( 70 |
71 |

Parent

72 | {this.props.children} 73 |
74 | ); 75 | } 76 | } 77 | 78 | class Child extends Component { 79 | render() { 80 | const { params: { id }} = this.props; 81 | 82 | return ( 83 |
84 |

Child

85 | {id &&

{id}

} 86 |
87 | ); 88 | } 89 | } 90 | 91 | const reducer = combineReducers({ 92 | router: routerStateReducer 93 | }); 94 | 95 | const store = compose( 96 | reduxReactRouter({ createHistory }), 97 | devTools() 98 | )(createStore)(reducer); 99 | 100 | class Root extends Component { 101 | render() { 102 | return ( 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 | ); 119 | } 120 | } 121 | 122 | ReactDOM.render(, document.getElementById('root')); 123 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-router-basic-example", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node -r babel/register server.js" 9 | }, 10 | "author": "Andrew Clark ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "redux-router": "^1.0.0-beta8" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import webpack from 'webpack'; 3 | import config from './webpack.config.clientDev'; 4 | import path from 'path'; 5 | 6 | import webpackDevMiddleware from 'webpack-dev-middleware'; 7 | import webpackHotMiddleware from 'webpack-hot-middleware'; 8 | 9 | const app = express(); 10 | const compiler = webpack(config); 11 | 12 | app.use(webpackDevMiddleware(compiler, { 13 | noInfo: true, 14 | publicPath: config.output.publicPath 15 | })); 16 | 17 | app.use(webpackHotMiddleware(compiler)); 18 | 19 | app.get('*', (req, res) => { 20 | res.sendFile(path.join(__dirname, 'index.html')); 21 | }); 22 | 23 | app.listen(3000, 'localhost', error => { 24 | if (error) { 25 | console.log(error); 26 | return; 27 | } 28 | 29 | console.log('Listening at http://localhost:3000'); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/basic/webpack.config.clientDev.js: -------------------------------------------------------------------------------- 1 | import { 2 | HotModuleReplacementPlugin, 3 | NoErrorsPlugin 4 | } from 'webpack'; 5 | 6 | import baseConfig from '../webpack.config.base'; 7 | import mergeConfig from '../mergeConfig'; 8 | import path from 'path'; 9 | 10 | const clientDevConfig = mergeConfig(baseConfig, { 11 | entry: [ 12 | 'webpack-hot-middleware/client', 13 | './index' 14 | ], 15 | output: { 16 | path: path.resolve(__dirname, 'build'), 17 | filename: 'bundle.js', 18 | publicPath: '/static/' 19 | }, 20 | plugins: [ 21 | new HotModuleReplacementPlugin(), 22 | new NoErrorsPlugin() 23 | ], 24 | devtool: 'eval' 25 | }); 26 | 27 | export default clientDevConfig; 28 | -------------------------------------------------------------------------------- /examples/mergeConfig.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/object/merge'; 2 | 3 | export default function mergeConfig(...configs) { 4 | return merge({}, ...configs, (a, b) => { 5 | if (Array.isArray(a)) { 6 | return a.concat(b); 7 | } 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-router-examples", 3 | "version": "0.1.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Andrew Clark ", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "babel": "^5.8.23", 12 | "babel-core": "^5.8.23", 13 | "babel-loader": "^5.3.2", 14 | "babel-plugin-react-transform": "^1.0.3", 15 | "babel-runtime": "^5.8.20", 16 | "express": "^4.13.3", 17 | "lodash": "^3.10.1", 18 | "query-string": "^2.4.1", 19 | "react-transform-hmr": "^1.0.1", 20 | "redux": "^2.0.0", 21 | "serialize-javascript": "^1.1.2", 22 | "webpack": "^1.12.1", 23 | "webpack-dev-middleware": "^1.2.0", 24 | "webpack-hot-middleware": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/server-rendering/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, compose } from 'redux'; 4 | 5 | import { 6 | ReduxRouter, 7 | reduxReactRouter, 8 | } from 'redux-router'; 9 | 10 | import { Provider } from 'react-redux'; 11 | import { devTools } from 'redux-devtools'; 12 | import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react'; 13 | import createHistory from 'history/lib/createBrowserHistory'; 14 | 15 | import routes from './routes'; 16 | import reducer from './reducer'; 17 | import {MOUNT_ID} from './constants'; 18 | 19 | const store = compose( 20 | reduxReactRouter({ createHistory }), 21 | devTools() 22 | )(createStore)(reducer, window.__initialState); 23 | 24 | const rootComponent = ( 25 | 26 | 27 | 28 | ); 29 | 30 | const mountNode = document.getElementById(MOUNT_ID); 31 | 32 | // First render to match markup from server 33 | ReactDOM.render(rootComponent, mountNode); 34 | // Optional second render with dev-tools 35 | ReactDOM.render(( 36 |
37 | {rootComponent} 38 | 39 | 40 | 41 |
42 | ), mountNode); 43 | -------------------------------------------------------------------------------- /examples/server-rendering/components.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {Link} from 'react-router'; 4 | 5 | @connect(state => ({ routerState: state.router })) 6 | export const App = class App extends Component { 7 | static propTypes = { 8 | children: PropTypes.node 9 | } 10 | 11 | render() { 12 | // Display is only used for rendering, its not a property of 13 | const links = [ 14 | { pathname: '/', display: '/'}, 15 | { pathname: '/parent', query: { foo: 'bar' }, display: '/parent?foo=bar'}, 16 | { pathname: '/parent/child', query: { bar: 'baz' }, display: '/parent/child?bar=baz'}, 17 | { pathname: '/parent/child/123', query: { baz: 'foo' }, display: '/parent/child/123?baz=foo'} 18 | ].map((l, i) => 19 |

20 | {l.display} 21 |

22 | ); 23 | 24 | return ( 25 |
26 |

App Container

27 | {links} 28 | {this.props.children} 29 |
30 | ); 31 | } 32 | }; 33 | 34 | export const Parent = class Parent extends Component { 35 | static propTypes = { 36 | children: PropTypes.node 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |

Parent

43 | {this.props.children} 44 |
45 | ); 46 | } 47 | }; 48 | 49 | export const Child = class Child extends Component { 50 | render() { 51 | const { params: { id }} = this.props; 52 | 53 | return ( 54 |
55 |

Child

56 | {id &&

{id}

} 57 |
58 | ); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /examples/server-rendering/constants.js: -------------------------------------------------------------------------------- 1 | export const MOUNT_ID = 'root'; 2 | -------------------------------------------------------------------------------- /examples/server-rendering/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-router-server-rendering-example", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node -r babel/register server.js" 9 | }, 10 | "author": "Andrew Clark ", 11 | "license": "MIT", 12 | "dependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /examples/server-rendering/reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import {routerStateReducer} from '../../lib'; // 'redux-router'; 3 | 4 | export default combineReducers({ 5 | router: routerStateReducer 6 | }); 7 | -------------------------------------------------------------------------------- /examples/server-rendering/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route} from 'react-router'; 3 | import {App, Parent, Child} from './components'; 4 | 5 | export default ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/server-rendering/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import webpack from 'webpack'; 3 | import React from 'react'; 4 | import {renderToString} from 'react-dom/server'; 5 | import {Provider} from 'react-redux'; 6 | import {createStore} from 'redux'; 7 | import {ReduxRouter} from '../../src'; // 'redux-router' 8 | import {reduxReactRouter, match} from '../../src/server'; // 'redux-router/server'; 9 | import qs from 'query-string'; 10 | import serialize from 'serialize-javascript'; 11 | import { createMemoryHistory } from 'history'; 12 | 13 | import webpackDevMiddleware from 'webpack-dev-middleware'; 14 | import webpackHotMiddleware from 'webpack-hot-middleware'; 15 | 16 | import config from './webpack.config.clientDev'; 17 | import {MOUNT_ID} from './constants'; 18 | import reducer from './reducer'; 19 | import routes from './routes'; 20 | 21 | const app = express(); 22 | const compiler = webpack(config); 23 | 24 | const getMarkup = (store) => { 25 | const initialState = serialize(store.getState()); 26 | 27 | const markup = renderToString( 28 | 29 | 30 | 31 | ); 32 | 33 | return ` 34 | 35 | 36 | Redux React Router – Server rendering Example 37 | 38 | 39 |
${markup}
40 | 41 | 42 | 43 | 44 | `; 45 | }; 46 | 47 | app.use(webpackDevMiddleware(compiler, { 48 | noInfo: true, 49 | publicPath: config.output.publicPath 50 | })); 51 | 52 | app.use(webpackHotMiddleware(compiler)); 53 | 54 | app.use((req, res) => { 55 | const store = reduxReactRouter({ routes, createHistory: createMemoryHistory })(createStore)(reducer); 56 | const query = qs.stringify(req.query); 57 | const url = req.path + (query.length ? '?' + query : ''); 58 | 59 | store.dispatch(match(url, (error, redirectLocation, routerState) => { 60 | if (error) { 61 | console.error('Router error:', error); 62 | res.status(500).send(error.message); 63 | } else if (redirectLocation) { 64 | res.redirect(302, redirectLocation.pathname + redirectLocation.search); 65 | } else if (!routerState) { 66 | res.status(400).send('Not Found'); 67 | } else { 68 | res.status(200).send(getMarkup(store)); 69 | } 70 | })); 71 | }); 72 | 73 | app.listen(3000, 'localhost', error => { 74 | if (error) { 75 | console.log(error); 76 | return; 77 | } 78 | 79 | console.log('Listening at http://localhost:3000'); 80 | }); 81 | -------------------------------------------------------------------------------- /examples/server-rendering/webpack.config.clientDev.js: -------------------------------------------------------------------------------- 1 | import { 2 | HotModuleReplacementPlugin, 3 | NoErrorsPlugin 4 | } from 'webpack'; 5 | 6 | import baseConfig from '../webpack.config.base'; 7 | import mergeConfig from '../mergeConfig'; 8 | import path from 'path'; 9 | 10 | const clientDevConfig = mergeConfig(baseConfig, { 11 | entry: [ 12 | 'webpack-hot-middleware/client', 13 | './client' 14 | ], 15 | output: { 16 | path: path.resolve(__dirname, 'build'), 17 | filename: 'bundle.js', 18 | publicPath: '/static/' 19 | }, 20 | plugins: [ 21 | new HotModuleReplacementPlugin(), 22 | new NoErrorsPlugin() 23 | ], 24 | devtool: 'eval' 25 | }); 26 | 27 | export default clientDevConfig; 28 | -------------------------------------------------------------------------------- /examples/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const PROJECT_SRC = path.resolve(__dirname, '../src'); 5 | 6 | const babelrc = fs.readFileSync(path.join('..', '.babelrc')); 7 | let babelLoaderQuery = {}; 8 | 9 | try { 10 | babelLoaderQuery = JSON.parse(babelrc); 11 | } catch (err) { 12 | console.error('Error parsing .babelrc.'); 13 | console.error(err); 14 | } 15 | babelLoaderQuery.plugins = babelLoaderQuery.plugins || []; 16 | babelLoaderQuery.plugins.push('react-transform'); 17 | babelLoaderQuery.extra = babelLoaderQuery.extra || {}; 18 | babelLoaderQuery.extra['react-transform'] = { 19 | transforms: [{ 20 | transform: 'react-transform-hmr', 21 | imports: ['react'], 22 | locals: ['module'] 23 | }] 24 | }; 25 | 26 | export default { 27 | module: { 28 | loaders: [{ 29 | test: /\.js$/, 30 | loader: 'babel', 31 | query: babelLoaderQuery, 32 | exclude: path.resolve(__dirname, 'node_modules'), 33 | include: [ 34 | path.resolve(__dirname), 35 | PROJECT_SRC 36 | ] 37 | }] 38 | }, 39 | resolve: { 40 | alias: { 41 | 'redux-router': PROJECT_SRC 42 | }, 43 | extensions: ['', '.js'] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-router", 3 | "version": "2.1.2", 4 | "description": "Redux bindings for React Router — keep your router state inside your Redux Store.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min", 8 | "build:commonjs": "babel src --out-dir lib && cp lib/serverModule.js server.js", 9 | "build:umd": "NODE_ENV=development browserify -s ReduxRouter --detect-globals lib/index.js -o dist/redux-router.js", 10 | "build:umd:min": "NODE_ENV=production browserify -s ReduxRouter --detect-globals lib/index.js | uglifyjs -c warnings=false -m > dist/redux-router.min.js", 11 | "clean": "rimraf lib && rimraf dist && rimraf server.js", 12 | "lint": "eslint src", 13 | "test": "mocha --compilers js:babel/register --recursive --require src/__tests__/init.js src/**/*-test.js", 14 | "test-watch": "mocha --compilers js:babel/register --recursive --require src/__tests__/init.js -w src/**/*-test.js", 15 | "prepublish": "npm run clean && mkdir dist && npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/acdlite/redux-router.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "react-router", 24 | "react", 25 | "router" 26 | ], 27 | "author": "Andrew Clark ", 28 | "license": "MIT", 29 | "files": [ 30 | "dist", 31 | "lib", 32 | "src", 33 | "LICENSE", 34 | "*.md", 35 | "server.js" 36 | ], 37 | "devDependencies": { 38 | "babel": "^5.6.14", 39 | "babel-core": "5.6.15", 40 | "babel-eslint": "^4.1.1", 41 | "babel-loader": "^5.3.2", 42 | "browserify": "^13.0.1", 43 | "chai": "^3.0.0", 44 | "eslint": "^1.3.1", 45 | "eslint-config-airbnb": "0.0.8", 46 | "eslint-plugin-react": "^3.3.1", 47 | "history": "^2.0.0", 48 | "jsdom": "^5.6.0", 49 | "mocha": "^2.2.5", 50 | "mocha-jsdom": "^1.0.0", 51 | "node-libs-browser": "^0.5.2", 52 | "react": "^0.14.1", 53 | "react-addons-test-utils": "^0.14.1", 54 | "react-dom": "^0.14.1", 55 | "react-redux": "^4.0.0", 56 | "react-router": "^2.0.0", 57 | "react-transform-hmr": "^1.0.1", 58 | "redux": "^3.0.0", 59 | "redux-devtools": "^2.1.0", 60 | "rimraf": "^2.4.3", 61 | "sinon": "^1.15.4", 62 | "uglify-js": "^2.6.2", 63 | "webpack": "^1.12.1" 64 | }, 65 | "dependencies": { 66 | "deep-equal": "^1.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ReduxRouter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { RouterContext as DefaultRoutingContext } from 'react-router'; 4 | import { createRouterObject } from 'react-router/lib/RouterUtils'; 5 | import routerStateEquals from './routerStateEquals'; 6 | import { ROUTER_STATE_SELECTOR } from './constants'; 7 | import { initRoutes, replaceRoutes } from './actionCreators'; 8 | 9 | function memoizeRouterStateSelector(selector) { 10 | let previousRouterState = null; 11 | 12 | return state => { 13 | const nextRouterState = selector(state); 14 | if (routerStateEquals(previousRouterState, nextRouterState)) { 15 | return previousRouterState; 16 | } 17 | previousRouterState = nextRouterState; 18 | return nextRouterState; 19 | }; 20 | } 21 | 22 | function getRoutesFromProps(props) { 23 | return props.routes || props.children; 24 | } 25 | 26 | class ReduxRouter extends Component { 27 | static propTypes = { 28 | children: PropTypes.node 29 | } 30 | 31 | static contextTypes = { 32 | store: PropTypes.object 33 | } 34 | 35 | constructor(props, context) { 36 | super(props, context); 37 | this.router = createRouterObject(context.store.history, context.store.transitionManager); 38 | } 39 | 40 | componentWillMount() { 41 | this.context.store.dispatch(initRoutes(getRoutesFromProps(this.props))); 42 | } 43 | 44 | componentWillReceiveProps(nextProps) { 45 | this.receiveRoutes(getRoutesFromProps(nextProps)); 46 | } 47 | 48 | receiveRoutes(routes) { 49 | if (!routes) return; 50 | 51 | const { store } = this.context; 52 | store.dispatch(replaceRoutes(routes)); 53 | } 54 | 55 | render() { 56 | const { store } = this.context; 57 | 58 | if (!store) { 59 | throw new Error( 60 | 'Redux store missing from context of . Make sure you\'re ' 61 | + 'using a ' 62 | ); 63 | } 64 | 65 | const { 66 | history, 67 | [ROUTER_STATE_SELECTOR]: routerStateSelector 68 | } = store; 69 | 70 | if (!history || !routerStateSelector) { 71 | throw new Error( 72 | 'Redux store not configured properly for . Make sure ' 73 | + 'you\'re using the reduxReactRouter() store enhancer.' 74 | ); 75 | } 76 | 77 | return ( 78 | 84 | ); 85 | } 86 | } 87 | 88 | @connect( 89 | (state, { routerStateSelector }) => routerStateSelector(state) || {} 90 | ) 91 | class ReduxRouterContext extends Component { 92 | static propTypes = { 93 | location: PropTypes.object, 94 | RoutingContext: PropTypes.func 95 | } 96 | 97 | render() { 98 | const {location} = this.props; 99 | 100 | if (location === null || location === undefined) { 101 | return null; // Async matching 102 | } 103 | 104 | const RoutingContext = this.props.RoutingContext || DefaultRoutingContext; 105 | 106 | return ; 107 | } 108 | } 109 | 110 | export default ReduxRouter; 111 | -------------------------------------------------------------------------------- /src/__tests__/ReduxRouter-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | push, 3 | ReduxRouter, 4 | reduxReactRouter, 5 | routerStateReducer, 6 | } from '../'; 7 | 8 | import * as server from '../server'; 9 | 10 | import React, { Component, PropTypes } from 'react'; 11 | import { renderToString } from 'react-dom/server'; 12 | import { 13 | renderIntoDocument, 14 | findRenderedComponentWithType, 15 | findRenderedDOMComponentWithTag, 16 | Simulate 17 | } from 'react-addons-test-utils'; 18 | import { Provider, connect } from 'react-redux'; 19 | import { createStore, combineReducers } from 'redux'; 20 | import createHistory from 'history/lib/createMemoryHistory'; 21 | import { Link, Route, RouterContext } from 'react-router'; 22 | import jsdom from 'mocha-jsdom'; 23 | import sinon from 'sinon'; 24 | 25 | @connect(state => state.router) 26 | class App extends Component { 27 | static propTypes = { 28 | children: PropTypes.node, 29 | location: PropTypes.object 30 | } 31 | 32 | render() { 33 | const { location, children } = this.props; 34 | return ( 35 |
36 |

{`Pathname: ${location.pathname}`}

37 | {children} 38 |
39 | ); 40 | } 41 | } 42 | 43 | class Parent extends Component { 44 | static propTypes = { 45 | children: PropTypes.node 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | 52 | {this.props.children} 53 |
54 | ); 55 | } 56 | } 57 | 58 | class Child extends Component { 59 | render() { 60 | return ( 61 |
62 | ); 63 | } 64 | } 65 | 66 | function redirectOnEnter(pathname) { 67 | return (routerState, replace) => replace(null, pathname); 68 | } 69 | 70 | const routes = ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | 80 | 81 | describe('', () => { 82 | jsdom(); 83 | 84 | function renderApp() { 85 | const reducer = combineReducers({ 86 | router: routerStateReducer 87 | }); 88 | 89 | const history = createHistory(); 90 | const store = reduxReactRouter({ 91 | history 92 | })(createStore)(reducer); 93 | 94 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 95 | 96 | return renderIntoDocument( 97 | 98 | 99 | {routes} 100 | 101 | 102 | ); 103 | } 104 | 105 | it('renders a React Router app using state from a Redux ', () => { 106 | const tree = renderApp(); 107 | 108 | const child = findRenderedComponentWithType(tree, Child); 109 | expect(child.props.location.pathname).to.equal('/parent/child/123'); 110 | expect(child.props.location.query).to.eql({ key: 'value' }); 111 | expect(child.props.params).to.eql({ id: '123' }); 112 | }); 113 | 114 | it('only renders once on initial load', () => { 115 | const reducer = combineReducers({ 116 | router: routerStateReducer 117 | }); 118 | 119 | const history = createHistory(); 120 | const store = reduxReactRouter({ 121 | history 122 | })(createStore)(reducer); 123 | 124 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 125 | 126 | const historySpy = sinon.spy(); 127 | history.listen(() => historySpy()); 128 | 129 | renderIntoDocument( 130 | 131 | 132 | {routes} 133 | 134 | 135 | ); 136 | 137 | expect(historySpy.callCount).to.equal(1); 138 | }); 139 | 140 | it('should accept React.Components for "RoutingContext" prop of ReduxRouter', () => { 141 | const reducer = combineReducers({ 142 | router: routerStateReducer 143 | }); 144 | 145 | const history = createHistory(); 146 | const store = reduxReactRouter({ 147 | history 148 | })(createStore)(reducer); 149 | 150 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 151 | 152 | const consoleErrorSpy = sinon.spy(console, 'error'); 153 | 154 | renderIntoDocument( 155 | 156 | 157 | {routes} 158 | 159 | 160 | ); 161 | 162 | console.error.restore(); // eslint-disable-line no-console 163 | 164 | expect(consoleErrorSpy.called).to.be.false; 165 | }); 166 | 167 | it('should accept stateless React components for "RoutingContext" prop of ReduxRouter', () => { 168 | const reducer = combineReducers({ 169 | router: routerStateReducer 170 | }); 171 | 172 | const history = createHistory(); 173 | const store = reduxReactRouter({ 174 | history 175 | })(createStore)(reducer); 176 | 177 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 178 | 179 | const consoleErrorSpy = sinon.spy(console, 'error'); 180 | 181 | renderIntoDocument( 182 | 183 | }> 184 | {routes} 185 | 186 | 187 | ); 188 | 189 | console.error.restore(); // eslint-disable-line no-console 190 | 191 | expect(consoleErrorSpy.called).to.be.false; 192 | }); 193 | 194 | it('should not accept non-React-components for "RoutingContext" prop of ReduxRouter', () => { 195 | const reducer = combineReducers({ 196 | router: routerStateReducer 197 | }); 198 | 199 | const history = createHistory(); 200 | const store = reduxReactRouter({ 201 | history 202 | })(createStore)(reducer); 203 | 204 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 205 | 206 | class CustomRouterContext extends React.Component { 207 | render() { 208 | return ; 209 | } 210 | } 211 | 212 | const consoleErrorSpy = sinon.spy(console, 'error'); 213 | 214 | const render = () => renderIntoDocument( 215 | 216 | 217 | {routes} 218 | 219 | 220 | ); 221 | 222 | const invalidElementTypeErrorMessage = 'Element type is invalid: expected a string (for built-in components) ' + 223 | 'or a class/function (for composite components) but got: object. ' + 224 | 'Check the render method of `ReduxRouterContext`.'; 225 | 226 | const routingContextInvalidPropErrorMessage = 'Invalid prop `RoutingContext` of type `object` supplied to `ReduxRouterContext`'; 227 | 228 | const routingContextInvalidElementTypeErrorMessage = 'React.createElement: type should not be null, undefined, boolean, or number. ' + 229 | 'It should be a string (for DOM elements) or a ReactClass (for composite components). ' + 230 | 'Check the render method of `ReduxRouterContext`.'; 231 | 232 | expect(render).to.throw(invalidElementTypeErrorMessage); 233 | 234 | console.error.restore(); // eslint-disable-line no-console 235 | 236 | expect(consoleErrorSpy.calledTwice).to.be.true; 237 | 238 | expect(consoleErrorSpy.args[0][0]).to.contain(routingContextInvalidPropErrorMessage); 239 | expect(consoleErrorSpy.args[1][0]).to.contain(routingContextInvalidElementTypeErrorMessage); 240 | }); 241 | 242 | // does stuff inside `onClick` that makes it difficult to test. 243 | // They work in the example. 244 | // TODO: Refer to React Router tests once they're completed 245 | it.skip('works with ', () => { 246 | const tree = renderApp(); 247 | 248 | const child = findRenderedComponentWithType(tree, Child); 249 | expect(child.props.location.pathname).to.equal('/parent/child/123'); 250 | const link = findRenderedDOMComponentWithTag(tree, 'a'); 251 | 252 | Simulate.click(link); 253 | expect(child.props.location.pathname).to.equal('/parent/child/321'); 254 | }); 255 | 256 | describe('server-side rendering', () => { 257 | it('works', () => { 258 | const reducer = combineReducers({ 259 | router: routerStateReducer 260 | }); 261 | 262 | const store = server.reduxReactRouter({ routes, createHistory })(createStore)(reducer); 263 | store.dispatch(server.match('/parent/child/850?key=value', (err, redirectLocation, routerState) => { 264 | const output = renderToString( 265 | 266 | 267 | 268 | ); 269 | expect(output).to.match(/Pathname: \/parent\/child\/850/); 270 | expect(routerState.location.query).to.eql({ key: 'value' }); 271 | })); 272 | }); 273 | 274 | it('should gracefully handle 404s', () => { 275 | const reducer = combineReducers({ 276 | router: routerStateReducer 277 | }); 278 | 279 | const store = server.reduxReactRouter({ routes, createHistory })(createStore)(reducer); 280 | expect(() => store.dispatch(server.match('/404', () => {}))) 281 | .to.not.throw(); 282 | }); 283 | 284 | it('throws if routes are not passed to store enhancer', () => { 285 | const reducer = combineReducers({ 286 | router: routerStateReducer 287 | }); 288 | 289 | expect(() => server.reduxReactRouter()(createStore)(reducer)) 290 | .to.throw( 291 | 'When rendering on the server, routes must be passed to the ' 292 | + 'reduxReactRouter() store enhancer; routes as a prop or as children ' 293 | + 'of is not supported. To deal with circular ' 294 | + 'dependencies between routes and the store, use the ' 295 | + 'option getRoutes(store).' 296 | ); 297 | }); 298 | 299 | it('throws if createHistory is not passed to store enhancer', () => { 300 | const reducer = combineReducers({ 301 | router: routerStateReducer 302 | }); 303 | 304 | expect(() => server.reduxReactRouter({ routes })(createStore)(reducer)) 305 | .to.throw( 306 | 'When rendering on the server, createHistory must be passed to the ' 307 | + 'reduxReactRouter() store enhancer' 308 | ); 309 | }); 310 | 311 | it('handles redirects', () => { 312 | const reducer = combineReducers({ 313 | router: routerStateReducer 314 | }); 315 | 316 | const store = server.reduxReactRouter({ routes, createHistory })(createStore)(reducer); 317 | store.dispatch(server.match('/redirect', (error, redirectLocation) => { 318 | expect(error).to.be.null; 319 | expect(redirectLocation.pathname).to.equal('/parent/child/850'); 320 | })); 321 | }); 322 | }); 323 | 324 | describe('dynamic route switching', () => { 325 | it('updates routes wnen receives new props', () => { 326 | const newRoutes = ( 327 | 328 | ); 329 | 330 | const reducer = combineReducers({ 331 | router: routerStateReducer 332 | }); 333 | 334 | const history = createHistory(); 335 | const store = reduxReactRouter({ history })(createStore)(reducer); 336 | 337 | class RouterContainer extends Component { 338 | state = { routes } 339 | 340 | render() { 341 | return ( 342 | 343 | 344 | 345 | ); 346 | } 347 | } 348 | 349 | store.dispatch(push({ pathname: '/parent/child' })); 350 | const tree = renderIntoDocument(); 351 | 352 | 353 | expect(store.getState().router.params).to.eql({}); 354 | tree.setState({ routes: newRoutes }); 355 | expect(store.getState().router.params).to.eql({ route: 'child' }); 356 | }); 357 | }); 358 | }); 359 | -------------------------------------------------------------------------------- /src/__tests__/init.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | global.expect = chai.expect; 3 | -------------------------------------------------------------------------------- /src/__tests__/reduxReactRouter-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | reduxReactRouter, 3 | routerStateReducer, 4 | push, 5 | replace, 6 | isActive 7 | } from '../'; 8 | import { REPLACE_ROUTES } from '../constants'; 9 | 10 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux'; 11 | import React from 'react'; 12 | import { Route } from 'react-router'; 13 | import createHistory from 'history/lib/createMemoryHistory'; 14 | import useBasename from 'history/lib/useBasename'; 15 | import sinon from 'sinon'; 16 | 17 | const routes = ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | describe('reduxRouter()', () => { 26 | it('adds router state to Redux store', () => { 27 | const reducer = combineReducers({ 28 | router: routerStateReducer 29 | }); 30 | 31 | const store = reduxReactRouter({ 32 | createHistory, 33 | routes 34 | })(createStore)(reducer); 35 | 36 | const history = store.history; 37 | 38 | const historySpy = sinon.spy(); 39 | history.listen(() => historySpy()); 40 | 41 | expect(historySpy.callCount).to.equal(1); 42 | 43 | 44 | store.dispatch(push({ pathname: '/parent' })); 45 | expect(store.getState().router.location.pathname).to.equal('/parent'); 46 | expect(historySpy.callCount).to.equal(2); 47 | 48 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 49 | expect(historySpy.callCount).to.equal(3); 50 | expect(store.getState().router.location.pathname) 51 | .to.equal('/parent/child/123'); 52 | expect(store.getState().router.location.query).to.eql({ key: 'value' }); 53 | expect(store.getState().router.params).to.eql({ id: '123' }); 54 | }); 55 | 56 | it('detects external router state changes', () => { 57 | const baseReducer = combineReducers({ 58 | router: routerStateReducer 59 | }); 60 | 61 | const EXTERNAL_STATE_CHANGE = 'EXTERNAL_STATE_CHANGE'; 62 | 63 | const externalState = { 64 | location: { 65 | pathname: '/parent/child/123', 66 | query: { key: 'value' }, 67 | key: 'lolkey' 68 | } 69 | }; 70 | 71 | const reducerSpy = sinon.spy(); 72 | function reducer(state, action) { 73 | reducerSpy(); 74 | 75 | if (action.type === EXTERNAL_STATE_CHANGE) { 76 | return { ...state, router: action.payload }; 77 | } 78 | 79 | return baseReducer(state, action); 80 | } 81 | 82 | const history = createHistory(); 83 | const historySpy = sinon.spy(); 84 | 85 | let historyState; 86 | history.listen(s => { 87 | historySpy(); 88 | historyState = s; 89 | }); 90 | 91 | const store = reduxReactRouter({ 92 | history, 93 | routes 94 | })(createStore)(reducer); 95 | 96 | expect(reducerSpy.callCount).to.equal(2); 97 | expect(historySpy.callCount).to.equal(1); 98 | 99 | store.dispatch({ 100 | type: EXTERNAL_STATE_CHANGE, 101 | payload: externalState 102 | }); 103 | 104 | expect(reducerSpy.callCount).to.equal(4); 105 | expect(historySpy.callCount).to.equal(2); 106 | expect(historyState.pathname).to.equal('/parent/child/123'); 107 | expect(historyState.search).to.equal('?key=value'); 108 | }); 109 | 110 | it('works with navigation action creators', () => { 111 | const reducer = combineReducers({ 112 | router: routerStateReducer 113 | }); 114 | 115 | const store = reduxReactRouter({ 116 | createHistory, 117 | routes 118 | })(createStore)(reducer); 119 | 120 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 121 | expect(store.getState().router.location.pathname) 122 | .to.equal('/parent/child/123'); 123 | expect(store.getState().router.location.query).to.eql({ key: 'value' }); 124 | expect(store.getState().router.params).to.eql({ id: '123' }); 125 | 126 | store.dispatch(replace({ pathname: '/parent/child/321', query: { key: 'value2'} })); 127 | expect(store.getState().router.location.pathname) 128 | .to.equal('/parent/child/321'); 129 | expect(store.getState().router.location.query).to.eql({ key: 'value2' }); 130 | expect(store.getState().router.params).to.eql({ id: '321' }); 131 | }); 132 | 133 | it('doesn\'t interfere with other actions', () => { 134 | const APPEND_STRING = 'APPEND_STRING'; 135 | 136 | function stringBuilderReducer(state = '', action) { 137 | if (action.type === APPEND_STRING) { 138 | return state + action.string; 139 | } 140 | return state; 141 | } 142 | 143 | const reducer = combineReducers({ 144 | router: routerStateReducer, 145 | string: stringBuilderReducer 146 | }); 147 | 148 | const history = createHistory(); 149 | 150 | const store = reduxReactRouter({ 151 | history, 152 | routes 153 | })(createStore)(reducer); 154 | 155 | store.dispatch({ type: APPEND_STRING, string: 'Uni' }); 156 | store.dispatch({ type: APPEND_STRING, string: 'directional' }); 157 | expect(store.getState().string).to.equal('Unidirectional'); 158 | }); 159 | 160 | it('stores the latest state in routerState', () => { 161 | const reducer = combineReducers({ 162 | router: routerStateReducer 163 | }); 164 | 165 | const history = createHistory(); 166 | 167 | const store = reduxReactRouter({ 168 | history, 169 | routes 170 | })(createStore)(reducer); 171 | 172 | let historyState; 173 | history.listen(s => { 174 | historyState = s; 175 | }); 176 | 177 | store.dispatch(push({ pathname: '/parent' })); 178 | 179 | store.dispatch({ 180 | type: REPLACE_ROUTES 181 | }); 182 | 183 | historyState = null; 184 | 185 | store.dispatch({ type: 'RANDOM_ACTION' }); 186 | expect(historyState).to.equal(null); 187 | }); 188 | 189 | it('handles async middleware', (done) => { 190 | const reducer = combineReducers({ 191 | router: routerStateReducer 192 | }); 193 | 194 | const history = createHistory(); 195 | const historySpy = sinon.spy(); 196 | 197 | history.listen(() => historySpy()); 198 | expect(historySpy.callCount).to.equal(1); 199 | 200 | compose( 201 | reduxReactRouter({ 202 | history, 203 | routes, 204 | }), 205 | applyMiddleware( 206 | () => next => action => setTimeout(() => next(action), 0) 207 | ) 208 | )(createStore)(reducer); 209 | 210 | history.push({ pathname: '/parent' }); 211 | expect(historySpy.callCount).to.equal(2); 212 | 213 | setTimeout(() => { 214 | expect(historySpy.callCount).to.equal(2); 215 | done(); 216 | }, 0); 217 | }); 218 | 219 | it('accepts history object when using basename', () => { 220 | const reducer = combineReducers({ 221 | router: routerStateReducer 222 | }); 223 | 224 | const history = useBasename(createHistory)({ 225 | basename: '/grandparent' 226 | }); 227 | 228 | const store = reduxReactRouter({ 229 | history, 230 | routes 231 | })(createStore)(reducer); 232 | 233 | store.dispatch(push({ pathname: '/parent' })); 234 | expect(store.getState().router.location.pathname).to.eql('/parent'); 235 | 236 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 237 | expect(store.getState().router.location.pathname) 238 | .to.eql('/parent/child/123'); 239 | expect(store.getState().router.location.basename).to.eql('/grandparent'); 240 | expect(store.getState().router.location.query).to.eql({ key: 'value' }); 241 | expect(store.getState().router.params).to.eql({ id: '123' }); 242 | }); 243 | 244 | describe('getRoutes()', () => { 245 | it('is passed dispatch and getState', () => { 246 | const reducer = combineReducers({ 247 | router: routerStateReducer 248 | }); 249 | 250 | let store; 251 | const history = createHistory(); 252 | 253 | reduxReactRouter({ 254 | history, 255 | getRoutes: s => { 256 | store = s; 257 | return routes; 258 | } 259 | })(createStore)(reducer); 260 | 261 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 262 | expect(store.getState().router.location.pathname) 263 | .to.equal('/parent/child/123'); 264 | }); 265 | }); 266 | 267 | describe('onEnter hook', () => { 268 | it('can perform redirects', () => { 269 | const reducer = combineReducers({ 270 | router: routerStateReducer 271 | }); 272 | 273 | const history = createHistory(); 274 | 275 | const requireAuth = (nextState, redirect) => { 276 | redirect({ pathname: '/login' }); 277 | }; 278 | 279 | const store = reduxReactRouter({ 280 | history, 281 | routes: ( 282 | 283 | 284 | 285 | 286 | 287 | 288 | ) 289 | })(createStore)(reducer); 290 | 291 | store.dispatch(push({ pathname: '/parent/child/123', query: { key: 'value' } })); 292 | expect(store.getState().router.location.pathname) 293 | .to.equal('/login'); 294 | }); 295 | 296 | describe('isActive', () => { 297 | it('creates a selector for whether a pathname/query pair is active', () => { 298 | const reducer = combineReducers({ 299 | router: routerStateReducer 300 | }); 301 | 302 | const history = createHistory(); 303 | 304 | const store = reduxReactRouter({ 305 | history, 306 | routes 307 | })(createStore)(reducer); 308 | 309 | const activeSelector = isActive('/parent', { key: 'value' }); 310 | expect(activeSelector(store.getState().router)).to.be.false; 311 | store.dispatch(push({ pathname: '/parent', query: { key: 'value' } })); 312 | expect(activeSelector(store.getState().router)).to.be.true; 313 | }); 314 | }); 315 | }); 316 | }); 317 | -------------------------------------------------------------------------------- /src/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { ROUTER_DID_CHANGE, INIT_ROUTES, REPLACE_ROUTES, HISTORY_API } from './constants'; 2 | 3 | /** 4 | * Action creator for signaling that the router has changed. 5 | * @private 6 | * @param {RouterState} state - New router state 7 | * @return {Action} Action object 8 | */ 9 | export function routerDidChange(state) { 10 | return { 11 | type: ROUTER_DID_CHANGE, 12 | payload: state 13 | }; 14 | } 15 | 16 | /** 17 | * Action creator that initiates route config 18 | * @private 19 | * @param {Array|ReactElement} routes - New routes 20 | */ 21 | export function initRoutes(routes) { 22 | return { 23 | type: INIT_ROUTES, 24 | payload: routes 25 | }; 26 | } 27 | 28 | /** 29 | * Action creator that replaces the current route config 30 | * @private 31 | * @param {Array|ReactElement} routes - New routes 32 | */ 33 | export function replaceRoutes(routes) { 34 | return { 35 | type: REPLACE_ROUTES, 36 | payload: routes 37 | }; 38 | } 39 | 40 | /** 41 | * Creates an action creator for calling a history API method. 42 | * @param {string} method - Name of method 43 | * @returns {ActionCreator} Action creator with same parameters as corresponding 44 | * history method 45 | */ 46 | export function historyAPI(method) { 47 | return (...args) => ({ 48 | type: HISTORY_API, 49 | payload: { 50 | method, 51 | args 52 | } 53 | }); 54 | } 55 | 56 | export const push = historyAPI('push'); 57 | export const replace = historyAPI('replace'); 58 | export const setState = historyAPI('setState'); 59 | export const go = historyAPI('go'); 60 | export const goBack = historyAPI('goBack'); 61 | export const goForward = historyAPI('goForward'); 62 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { compose } from 'redux'; 2 | import { routerDidChange } from './actionCreators'; 3 | import routerStateEquals from './routerStateEquals'; 4 | import reduxReactRouter from './reduxReactRouter'; 5 | import useDefaults from './useDefaults'; 6 | import routeReplacement from './routeReplacement'; 7 | 8 | function historySynchronization(next) { 9 | return options => createStore => (reducer, initialState) => { 10 | const { onError, routerStateSelector } = options; 11 | const store = next(options)(createStore)(reducer, initialState); 12 | const { history, transitionManager } = store; 13 | 14 | let prevRouterState; 15 | let routerState; 16 | 17 | transitionManager.listen((error, nextRouterState) => { 18 | if (error) { 19 | onError(error); 20 | return; 21 | } 22 | 23 | if (!routerStateEquals(routerState, nextRouterState)) { 24 | prevRouterState = routerState; 25 | routerState = nextRouterState; 26 | store.dispatch(routerDidChange(nextRouterState)); 27 | } 28 | }); 29 | 30 | store.subscribe(() => { 31 | const nextRouterState = routerStateSelector(store.getState()); 32 | 33 | if ( 34 | nextRouterState && 35 | prevRouterState !== nextRouterState && 36 | !routerStateEquals(routerState, nextRouterState) 37 | ) { 38 | routerState = nextRouterState; 39 | const { state, pathname, query } = nextRouterState.location; 40 | history.replace({state, pathname, query}); 41 | } 42 | }); 43 | 44 | return store; 45 | }; 46 | } 47 | 48 | export default compose( 49 | useDefaults, 50 | routeReplacement, 51 | historySynchronization 52 | )(reduxReactRouter); 53 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // Signals that the router's state has changed. It should 2 | // never be called by the application, only as an implementation detail of 3 | // redux-react-router. 4 | export const ROUTER_DID_CHANGE = '@@reduxReactRouter/routerDidChange'; 5 | 6 | export const HISTORY_API = '@@reduxReactRouter/historyAPI'; 7 | export const MATCH = '@@reduxReactRouter/match'; 8 | export const INIT_ROUTES = '@@reduxReactRouter/initRoutes'; 9 | export const REPLACE_ROUTES = '@@reduxReactRouter/replaceRoutes'; 10 | 11 | export const ROUTER_STATE_SELECTOR = '@@reduxReactRouter/routerStateSelector'; 12 | 13 | export const DOES_NEED_REFRESH = '@@reduxReactRouter/doesNeedRefresh'; 14 | -------------------------------------------------------------------------------- /src/historyMiddleware.js: -------------------------------------------------------------------------------- 1 | import { HISTORY_API } from './constants'; 2 | 3 | /** 4 | * Middleware for interacting with the history API 5 | * @param {History} History object 6 | */ 7 | export default function historyMiddleware(history) { 8 | return () => next => action => { 9 | if (action.type === HISTORY_API) { 10 | const { method, args } = action.payload; 11 | return history[method](...args); 12 | } 13 | return next(action); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export routerStateReducer from './routerStateReducer'; 2 | export ReduxRouter from './ReduxRouter'; 3 | export reduxReactRouter from './client'; 4 | export isActive from './isActive'; 5 | 6 | export { 7 | historyAPI, 8 | push, 9 | replace, 10 | setState, 11 | go, 12 | goBack, 13 | goForward 14 | } from './actionCreators'; 15 | -------------------------------------------------------------------------------- /src/isActive.js: -------------------------------------------------------------------------------- 1 | import _isActive from 'react-router/lib/isActive'; 2 | 3 | /** 4 | * Creates a router state selector that returns whether or not the given 5 | * pathname and query are active. 6 | * @param {String} pathname 7 | * @param {Object} query 8 | * @param {Boolean} indexOnly 9 | * @return {Boolean} 10 | */ 11 | export default function isActive(pathname, query, indexOnly = false) { 12 | return state => { 13 | if (!state) return false; 14 | const { location, params, routes } = state; 15 | return _isActive({ pathname, query }, indexOnly, location, routes, params); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/matchMiddleware.js: -------------------------------------------------------------------------------- 1 | import { routerDidChange } from './actionCreators'; 2 | import { MATCH } from './constants'; 3 | 4 | export default function matchMiddleware(match) { 5 | return ({ dispatch }) => next => action => { 6 | if (action.type === MATCH) { 7 | const { url, callback } = action.payload; 8 | match(url, (error, redirectLocation, routerState) => { 9 | if (!error && !redirectLocation && routerState) { 10 | dispatch(routerDidChange(routerState)); 11 | } 12 | callback(error, redirectLocation, routerState); 13 | }); 14 | } 15 | return next(action); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/reduxReactRouter.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware } from 'redux'; 2 | import { useRouterHistory, createRoutes } from 'react-router'; 3 | import createTransitionManager from 'react-router/lib/createTransitionManager' ; 4 | import historyMiddleware from './historyMiddleware'; 5 | import { ROUTER_STATE_SELECTOR } from './constants'; 6 | 7 | export default function reduxReactRouter({ 8 | routes, 9 | createHistory, 10 | parseQueryString, 11 | stringifyQuery, 12 | routerStateSelector 13 | }) { 14 | return createStore => (reducer, initialState) => { 15 | 16 | let baseCreateHistory; 17 | if (typeof createHistory === 'function') { 18 | baseCreateHistory = createHistory; 19 | } else if (createHistory) { 20 | baseCreateHistory = () => createHistory; 21 | } 22 | 23 | const createAppHistory = useRouterHistory(baseCreateHistory); 24 | 25 | const history = createAppHistory({ 26 | parseQueryString, 27 | stringifyQuery, 28 | }); 29 | 30 | const transitionManager = createTransitionManager( 31 | history, createRoutes(routes) 32 | ); 33 | 34 | const store = 35 | applyMiddleware( 36 | historyMiddleware(history) 37 | )(createStore)(reducer, initialState); 38 | 39 | store.transitionManager = transitionManager; 40 | store.history = history; 41 | store[ROUTER_STATE_SELECTOR] = routerStateSelector; 42 | 43 | return store; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/replaceRoutesMiddleware.js: -------------------------------------------------------------------------------- 1 | import { INIT_ROUTES, REPLACE_ROUTES } from './constants'; 2 | 3 | export default function replaceRoutesMiddleware(replaceRoutes) { 4 | return () => next => action => { 5 | const isInitRoutes = action.type === INIT_ROUTES; 6 | if (isInitRoutes || action.type === REPLACE_ROUTES) { 7 | replaceRoutes(action.payload, isInitRoutes); 8 | } 9 | return next(action); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/routeReplacement.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose } from 'redux'; 2 | import { createRoutes } from 'react-router'; 3 | import replaceRoutesMiddleware from './replaceRoutesMiddleware'; 4 | 5 | export default function routeReplacement(next) { 6 | return options => createStore => (reducer, initialState) => { 7 | const { 8 | routes: baseRoutes, 9 | getRoutes, 10 | routerStateSelector 11 | } = options; 12 | 13 | let store; 14 | 15 | let childRoutes = []; 16 | let areChildRoutesResolved = false; 17 | const childRoutesCallbacks = []; 18 | 19 | function replaceRoutes(r, isInit) { 20 | childRoutes = createRoutes(r); 21 | 22 | const routerState = routerStateSelector(store.getState()); 23 | if (routerState && !isInit) { 24 | const { state, pathname, query } = routerState.location; 25 | store.history.replace({state, pathname, query}); 26 | } 27 | 28 | if (!areChildRoutesResolved) { 29 | areChildRoutesResolved = true; 30 | childRoutesCallbacks.forEach(cb => cb(null, childRoutes)); 31 | } 32 | } 33 | 34 | let routes; 35 | if (baseRoutes) { 36 | routes = baseRoutes; 37 | } else if (getRoutes) { 38 | routes = getRoutes({ 39 | dispatch: action => store.dispatch(action), 40 | getState: () => store.getState() 41 | }); 42 | } else { 43 | routes = [{ 44 | getChildRoutes: (location, cb) => { 45 | if (!areChildRoutesResolved) { 46 | childRoutesCallbacks.push(cb); 47 | return; 48 | } 49 | 50 | cb(null, childRoutes); 51 | } 52 | }]; 53 | } 54 | 55 | store = compose( 56 | applyMiddleware( 57 | replaceRoutesMiddleware(replaceRoutes) 58 | ), 59 | next({ 60 | ...options, 61 | routes: createRoutes(routes) 62 | }) 63 | )(createStore)(reducer, initialState); 64 | 65 | return store; 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/routerStateEquals.js: -------------------------------------------------------------------------------- 1 | import deepEqual from 'deep-equal'; 2 | import { DOES_NEED_REFRESH } from './constants'; 3 | 4 | /** 5 | * Check if two router states are equal. Ignores `location.key`. 6 | * @returns {Boolean} 7 | */ 8 | export default function routerStateEquals(a, b) { 9 | if (!a && !b) return true; 10 | if ((a && !b) || (!a && b)) return false; 11 | if (a[DOES_NEED_REFRESH] || b[DOES_NEED_REFRESH]) return false; 12 | 13 | return ( 14 | a.location.pathname === b.location.pathname && 15 | a.location.search === b.location.search && 16 | deepEqual(a.location.state, b.location.state) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/routerStateReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | ROUTER_DID_CHANGE, 3 | REPLACE_ROUTES, 4 | DOES_NEED_REFRESH 5 | } from './constants'; 6 | 7 | /** 8 | * Reducer of ROUTER_DID_CHANGE actions. Returns a state object 9 | * with { pathname, query, params, navigationType } 10 | * @param {Object} state - Previous state 11 | * @param {Object} action - Action 12 | * @return {Object} New state 13 | */ 14 | export default function routerStateReducer(state = null, action) { 15 | switch (action.type) { 16 | case ROUTER_DID_CHANGE: 17 | return action.payload; 18 | case REPLACE_ROUTES: 19 | if (!state) return state; 20 | return { 21 | ...state, 22 | [DOES_NEED_REFRESH]: true 23 | }; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import { compose, applyMiddleware } from 'redux'; 2 | import baseReduxReactRouter from './reduxReactRouter'; 3 | import useDefaults from './useDefaults'; 4 | import routeReplacement from './routeReplacement'; 5 | import matchMiddleware from './matchMiddleware'; 6 | import { MATCH } from './constants'; 7 | 8 | function serverInvariants(next) { 9 | return options => createStore => { 10 | if (!options || !(options.routes || options.getRoutes)) { 11 | throw new Error( 12 | 'When rendering on the server, routes must be passed to the ' 13 | + 'reduxReactRouter() store enhancer; routes as a prop or as children of ' 14 | + ' is not supported. To deal with circular dependencies ' 15 | + 'between routes and the store, use the option getRoutes(store).' 16 | ); 17 | } 18 | if (!options || !(options.createHistory)) { 19 | throw new Error( 20 | 'When rendering on the server, createHistory must be passed to the ' 21 | + 'reduxReactRouter() store enhancer' 22 | ); 23 | } 24 | 25 | return next(options)(createStore); 26 | }; 27 | } 28 | 29 | function matching(next) { 30 | return options => createStore => (reducer, initialState) => { 31 | const store = compose( 32 | applyMiddleware( 33 | matchMiddleware((url, callback) => { 34 | const location = store.history.createLocation(url); 35 | 36 | store.transitionManager.match(location, callback); 37 | }) 38 | ), 39 | next(options))(createStore)(reducer, initialState); 40 | return store; 41 | }; 42 | } 43 | 44 | export function match(url, callback) { 45 | return { 46 | type: MATCH, 47 | payload: { 48 | url, 49 | callback 50 | } 51 | }; 52 | } 53 | 54 | export const reduxReactRouter = compose( 55 | serverInvariants, 56 | useDefaults, 57 | routeReplacement, 58 | matching 59 | )(baseReduxReactRouter); 60 | -------------------------------------------------------------------------------- /src/serverModule.js: -------------------------------------------------------------------------------- 1 | // This file is copied to the root of the project to allow 2 | export { reduxReactRouter, match } from './lib/server'; 3 | -------------------------------------------------------------------------------- /src/useDefaults.js: -------------------------------------------------------------------------------- 1 | const defaults = { 2 | onError: error => { throw error; }, 3 | routerStateSelector: state => state.router 4 | }; 5 | 6 | export default function useDefaults(next) { 7 | return options => createStore => (reducer, initialState) => { 8 | const optionsWithDefaults = { ...defaults, ...options }; 9 | 10 | const { 11 | createHistory: baseCreateHistory, 12 | history: baseHistory, 13 | } = optionsWithDefaults; 14 | 15 | let createHistory; 16 | if (typeof baseCreateHistory === 'function') { 17 | createHistory = baseCreateHistory; 18 | } else if (baseHistory) { 19 | createHistory = () => baseHistory; 20 | } else { 21 | createHistory = null; 22 | } 23 | 24 | return next({ 25 | ...optionsWithDefaults, 26 | createHistory 27 | })(createStore)(reducer, initialState); 28 | }; 29 | } 30 | --------------------------------------------------------------------------------