Skip to content

Commit 4bb250d

Browse files
committed
RFC: syncHistoryWithStore
1 parent 511fe67 commit 4bb250d

File tree

11 files changed

+130
-35
lines changed

11 files changed

+130
-35
lines changed

examples/real-world/containers/App.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component, PropTypes } from 'react'
22
import { connect } from 'react-redux'
3-
import { push } from 'react-router-redux'
3+
import { browserHistory } from 'react-router'
44
import Explore from '../components/Explore'
55
import { resetErrorMessage } from '../actions'
66

@@ -17,7 +17,7 @@ class App extends Component {
1717
}
1818

1919
handleChange(nextValue) {
20-
this.props.push(`/${nextValue}`)
20+
browserHistory.push(`/${nextValue}`)
2121
}
2222

2323
renderErrorMessage() {
@@ -56,20 +56,18 @@ App.propTypes = {
5656
// Injected by React Redux
5757
errorMessage: PropTypes.string,
5858
resetErrorMessage: PropTypes.func.isRequired,
59-
push: PropTypes.func.isRequired,
6059
inputValue: PropTypes.string.isRequired,
6160
// Injected by React Router
6261
children: PropTypes.node
6362
}
6463

65-
function mapStateToProps(state) {
64+
function mapStateToProps(state, ownProps) {
6665
return {
6766
errorMessage: state.errorMessage,
68-
inputValue: state.routing.location.pathname.substring(1)
67+
inputValue: ownProps.location.pathname.substring(1)
6968
}
7069
}
7170

7271
export default connect(mapStateToProps, {
73-
resetErrorMessage,
74-
push
72+
resetErrorMessage
7573
})(App)

examples/real-world/containers/RepoPage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ RepoPage.propTypes = {
7272
loadStargazers: PropTypes.func.isRequired
7373
}
7474

75-
function mapStateToProps(state, props) {
76-
const { login, name } = props.params
75+
function mapStateToProps(state, ownProps) {
76+
const { login, name } = ownProps.params
7777
const {
7878
pagination: { stargazersByRepo },
7979
entities: { users, repos }

examples/real-world/containers/Root.dev.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import React, { Component, PropTypes } from 'react'
22
import { Provider } from 'react-redux'
33
import routes from '../routes'
44
import DevTools from './DevTools'
5-
import { Router, browserHistory } from 'react-router'
5+
import { Router } from 'react-router'
66

77
export default class Root extends Component {
88
render() {
9-
const { store } = this.props
9+
const { store, history } = this.props
1010
return (
1111
<Provider store={store}>
1212
<div>
13-
<Router history={browserHistory} routes={routes} />
13+
<Router history={history} routes={routes} />
1414
<DevTools />
1515
</div>
1616
</Provider>

examples/real-world/containers/Root.prod.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import React, { Component, PropTypes } from 'react'
22
import { Provider } from 'react-redux'
33
import routes from '../routes'
4-
import { Router, browserHistory } from 'react-router'
4+
import { Router } from 'react-router'
55

66
export default class Root extends Component {
77
render() {
8-
const { store } = this.props
8+
const { store, history } = this.props
99
return (
1010
<Provider store={store}>
11-
<Router history={browserHistory} routes={routes} />
11+
<Router history={history} routes={routes} />
1212
</Provider>
1313
)
1414
}

examples/real-world/containers/UserPage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ UserPage.propTypes = {
7272
loadStarred: PropTypes.func.isRequired
7373
}
7474

75-
function mapStateToProps(state, props) {
76-
const { login } = props.params
75+
function mapStateToProps(state, ownProps) {
76+
const { login } = ownProps.params
7777
const {
7878
pagination: { starredByUser },
7979
entities: { users, repos }

examples/real-world/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import 'babel-polyfill'
22
import React from 'react'
33
import { render } from 'react-dom'
4+
import { browserHistory } from 'react-router'
45
import Root from './containers/Root'
56
import configureStore from './store/configureStore'
7+
import { syncHistoryWithStore } from './syncHistoryWithStore'
68

79
const store = configureStore()
10+
const history = syncHistoryWithStore(browserHistory, store, {
11+
adjustUrlOnReplay: true
12+
})
813

914
render(
10-
<Root store={store} />,
15+
<Root store={store} history={history} />,
1116
document.getElementById('root')
1217
)

examples/real-world/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"react-dom": "^0.14.7",
2525
"react-redux": "^4.2.1",
2626
"react-router": "2.0.0-rc5",
27-
"react-router-redux": "^2.1.0",
2827
"redux": "^3.2.1",
2928
"redux-logger": "^2.4.0",
3029
"redux-thunk": "^1.0.3"

examples/real-world/reducers/index.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as ActionTypes from '../actions'
22
import merge from 'lodash/merge'
33
import paginate from './paginate'
4-
import { routeReducer } from 'react-router-redux'
4+
import { reducer as routing } from '../syncHistoryWithStore'
55
import { combineReducers } from 'redux'
66

77
// Updates an entity cache in response to any action with response.entities.
@@ -50,8 +50,7 @@ const rootReducer = combineReducers({
5050
entities,
5151
pagination,
5252
errorMessage,
53-
routing: routeReducer
53+
routing
5454
})
5555

56-
5756
export default rootReducer

examples/real-world/store/configureStore.dev.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
import { createStore, applyMiddleware, compose } from 'redux'
2-
import { syncHistory } from 'react-router-redux'
3-
import { browserHistory } from 'react-router'
4-
import DevTools from '../containers/DevTools'
52
import thunk from 'redux-thunk'
6-
import api from '../middleware/api'
73
import createLogger from 'redux-logger'
4+
import api from '../middleware/api'
85
import rootReducer from '../reducers'
9-
10-
const reduxRouterMiddleware = syncHistory(browserHistory)
6+
import DevTools from '../containers/DevTools'
117

128
export default function configureStore(initialState) {
139
const store = createStore(
1410
rootReducer,
1511
initialState,
1612
compose(
17-
applyMiddleware(thunk, api, reduxRouterMiddleware, createLogger()),
13+
applyMiddleware(thunk, api, createLogger()),
1814
DevTools.instrument()
1915
)
2016
)
2117

22-
// Required for replaying actions from devtools to work
23-
reduxRouterMiddleware.listenForReplays(store)
24-
2518
if (module.hot) {
2619
// Enable Webpack hot module replacement for reducers
2720
module.hot.accept('../reducers', () => {
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { createStore, applyMiddleware } from 'redux'
2-
import { syncHistory } from 'react-router-redux'
3-
import { browserHistory } from 'react-router'
42
import thunk from 'redux-thunk'
53
import api from '../middleware/api'
64
import rootReducer from '../reducers'
@@ -9,6 +7,6 @@ export default function configureStore(initialState) {
97
return createStore(
108
rootReducer,
119
initialState,
12-
applyMiddleware(thunk, api, syncHistory(browserHistory))
10+
applyMiddleware(thunk, api)
1311
)
1412
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
export const WILL_NAVIGATE = '@@react-router/WILL_NAVIGATE'
2+
3+
const initialState = {
4+
locationBeforeTransitions: null
5+
}
6+
7+
// Mount this reducer to handle location changes
8+
export const reducer = (state = initialState, action) => {
9+
switch (action.type) {
10+
case WILL_NAVIGATE:
11+
// Use a descriptive name to make it less tempting to reach into it
12+
return {
13+
locationBeforeTransitions: action.locationBeforeTransitions
14+
}
15+
default:
16+
return state
17+
}
18+
}
19+
20+
export function syncHistoryWithStore(history, store, {
21+
// Specify where you mounted the reducer
22+
selectLocationState = state => state.routing,
23+
adjustUrlOnReplay = false
24+
} = {}) {
25+
let initialLocation
26+
let currentLocation
27+
let isTimeTraveling
28+
let unsubscribeFromStore
29+
let unsubscribeFromHistory
30+
31+
// What does the store say about current location?
32+
const getLocationInStore = (useInitialIfEmpty) => {
33+
const locationState = selectLocationState(store.getState())
34+
return locationState.locationBeforeTransitions ||
35+
(useInitialIfEmpty ? initialLocation : undefined)
36+
}
37+
38+
// Whenever store changes due to time travel, keep address bar in sync
39+
const handleStoreChange = () => {
40+
const locationInStore = getLocationInStore(true)
41+
if (currentLocation === locationInStore) {
42+
return
43+
}
44+
45+
// Update address bar to reflect store state
46+
isTimeTraveling = true
47+
currentLocation = locationInStore
48+
history.replace(locationInStore)
49+
isTimeTraveling = false
50+
}
51+
52+
if (adjustUrlOnReplay) {
53+
unsubscribeFromStore = store.subscribe(handleStoreChange)
54+
handleStoreChange()
55+
}
56+
57+
// Whenever location changes, dispatch an action to get it in the store
58+
const handleLocationChange = (location) => {
59+
// ... unless we just caused that location change
60+
if (isTimeTraveling) {
61+
return
62+
}
63+
64+
// Remember where we are
65+
currentLocation = location
66+
67+
// Are we being called for the first time?
68+
if (!initialLocation) {
69+
// Remember as a fallback in case state is reset
70+
initialLocation = location
71+
72+
// Respect persisted location, if any
73+
if (getLocationInStore()) {
74+
return
75+
}
76+
}
77+
78+
// Tell the store to update by dispatching an action
79+
store.dispatch({
80+
type: WILL_NAVIGATE,
81+
locationBeforeTransitions: location
82+
})
83+
}
84+
unsubscribeFromHistory = history.listen(handleLocationChange)
85+
86+
// The enhanced history uses store as source of truth
87+
return Object.assign({}, history, {
88+
// The listeners are subscribed to the store instead of history
89+
listen(listener) {
90+
return store.subscribe(() =>
91+
listener(getLocationInStore(true))
92+
)
93+
},
94+
95+
// It also provides a way to destroy internal listeners
96+
dispose() {
97+
if (adjustUrlOnReplay) {
98+
unsubscribeFromStore()
99+
}
100+
unsubscribeFromHistory()
101+
}
102+
})
103+
}

0 commit comments

Comments
 (0)