Skip to content
This repository was archived by the owner on Oct 26, 2018. It is now read-only.

Commit 2946938

Browse files
committed
Pull in @gaearon's history syncer.
See reduxjs/redux#1362 Some small modifications and commenting added. Absolutely needs tests!
1 parent 7cdc72a commit 2946938

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export {
55
} from './actions'
66

77
export routerMiddleware from './middleware'
8+
9+
export { LOCATION_CHANGE, routerReducer, syncHistoryWithStore } from './sync'

src/sync.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* This action type will be dispatched when your history
3+
* receives a location change.
4+
*/
5+
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
6+
7+
const initialState = {
8+
locationBeforeTransitions: null
9+
}
10+
11+
const defaultSelectLocationState = state => state.routing
12+
13+
/**
14+
* This reducer will update the state with the most recent location history
15+
* has transitioned to. This may not be in sync with the router, particularly
16+
* if you have asynchronously-loaded routes, so reading from and relying on
17+
* this state it is discouraged.
18+
*/
19+
export function routerReducer(state = initialState, { type, locationBeforeTransitions }) {
20+
if (type === LOCATION_CHANGE) {
21+
return { ...state, locationBeforeTransitions }
22+
}
23+
24+
return state
25+
}
26+
27+
/**
28+
* This function synchronizes your history state with the Redux store.
29+
* Location changes flow from history to the store. An enhanced history is
30+
* returned with a listen method that responds to store updates for location.
31+
*
32+
* When this history is provided to the router, this means the location data
33+
* will flow like this:
34+
* history.push -> store.dispatch -> enhancedHistory.listen -> router
35+
* This ensures that when the store state changes due to a replay or other
36+
* event, the router will be updated appropriately and can transition to the
37+
* correct router state.
38+
*/
39+
export function syncHistoryWithStore(history, store, {
40+
selectLocationState = defaultSelectLocationState,
41+
adjustUrlOnReplay = true
42+
} = {}) {
43+
// Ensure that the reducer is mounted on the store and functioning properly.
44+
if (typeof selectLocationState(store.getState()) === 'undefined') {
45+
throw new Error(
46+
'Expected the routing state to be available either as `state.routing` ' +
47+
'or as the custom expression you can specify as `selectLocationState` ' +
48+
'in the `syncHistoryWithStore()` options. ' +
49+
'Ensure you have added the `routerReducer` to your store\'s ' +
50+
'reducers via `combineReducers` or whatever method you use to isolate ' +
51+
'your reducers.'
52+
)
53+
}
54+
55+
let initialLocation
56+
let currentLocation
57+
let isTimeTraveling
58+
let unsubscribeFromStore
59+
let unsubscribeFromHistory
60+
61+
// What does the store say about current location?
62+
const getLocationInStore = (useInitialIfEmpty) => {
63+
const locationState = selectLocationState(store.getState())
64+
return locationState.locationBeforeTransitions ||
65+
(useInitialIfEmpty ? initialLocation : undefined)
66+
}
67+
68+
// If the store is replayed, update the URL in the browser to match.
69+
if (adjustUrlOnReplay) {
70+
const handleStoreChange = () => {
71+
const locationInStore = getLocationInStore(true)
72+
if (currentLocation === locationInStore) {
73+
return
74+
}
75+
76+
// Update address bar to reflect store state
77+
isTimeTraveling = true
78+
currentLocation = locationInStore
79+
history.transitionTo(Object.assign({},
80+
locationInStore,
81+
{ action: 'PUSH' }
82+
))
83+
isTimeTraveling = false
84+
}
85+
86+
unsubscribeFromStore = store.subscribe(handleStoreChange)
87+
handleStoreChange()
88+
}
89+
90+
// Whenever location changes, dispatch an action to get it in the store
91+
const handleLocationChange = (location) => {
92+
// ... unless we just caused that location change
93+
if (isTimeTraveling) {
94+
return
95+
}
96+
97+
// Remember where we are
98+
currentLocation = location
99+
100+
// Are we being called for the first time?
101+
if (!initialLocation) {
102+
// Remember as a fallback in case state is reset
103+
initialLocation = location
104+
105+
// Respect persisted location, if any
106+
if (getLocationInStore()) {
107+
return
108+
}
109+
}
110+
111+
// Tell the store to update by dispatching an action
112+
store.dispatch({
113+
type: LOCATION_CHANGE,
114+
locationBeforeTransitions: location
115+
})
116+
}
117+
unsubscribeFromHistory = history.listen(handleLocationChange)
118+
119+
// The enhanced history uses store as source of truth
120+
return Object.assign({}, history, {
121+
// The listeners are subscribed to the store instead of history
122+
listen(listener) {
123+
// History listeners expect a synchronous call
124+
listener(getLocationInStore(true))
125+
126+
// Keep track of whether we unsubscribed, as Redux store
127+
// only applies changes in subscriptions on next dispatch
128+
let unsubscribed = false
129+
const unsubscribeFromStore = store.subscribe(() => {
130+
if (!unsubscribed) {
131+
listener(getLocationInStore(true))
132+
}
133+
})
134+
135+
// Let user unsubscribe later
136+
return () => {
137+
unsubscribed = true
138+
unsubscribeFromStore()
139+
}
140+
},
141+
142+
// It also provides a way to destroy internal listeners
143+
dispose() {
144+
if (adjustUrlOnReplay) {
145+
unsubscribeFromStore()
146+
}
147+
unsubscribeFromHistory()
148+
}
149+
})
150+
}

0 commit comments

Comments
 (0)