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

Commit

Permalink
Pull in @gaearon's history syncer.
Browse files Browse the repository at this point in the history
See reduxjs/redux#1362

Some small modifications and commenting added. Absolutely needs tests!
  • Loading branch information
timdorr committed Feb 5, 2016
1 parent 7cdc72a commit 2946938
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export {
} from './actions'

export routerMiddleware from './middleware'

export { LOCATION_CHANGE, routerReducer, syncHistoryWithStore } from './sync'
150 changes: 150 additions & 0 deletions src/sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* This action type will be dispatched when your history
* receives a location change.
*/
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'

const initialState = {
locationBeforeTransitions: null
}

const defaultSelectLocationState = state => state.routing

/**
* This reducer will update the state with the most recent location history
* has transitioned to. This may not be in sync with the router, particularly
* if you have asynchronously-loaded routes, so reading from and relying on
* this state it is discouraged.
*/
export function routerReducer(state = initialState, { type, locationBeforeTransitions }) {
if (type === LOCATION_CHANGE) {
return { ...state, locationBeforeTransitions }
}

return state
}

/**
* This function synchronizes your history state with the Redux store.
* Location changes flow from history to the store. An enhanced history is
* returned with a listen method that responds to store updates for location.
*
* When this history is provided to the router, this means the location data
* will flow like this:
* history.push -> store.dispatch -> enhancedHistory.listen -> router
* This ensures that when the store state changes due to a replay or other
* event, the router will be updated appropriately and can transition to the
* correct router state.
*/
export function syncHistoryWithStore(history, store, {
selectLocationState = defaultSelectLocationState,
adjustUrlOnReplay = true
} = {}) {
// Ensure that the reducer is mounted on the store and functioning properly.
if (typeof selectLocationState(store.getState()) === 'undefined') {
throw new Error(
'Expected the routing state to be available either as `state.routing` ' +
'or as the custom expression you can specify as `selectLocationState` ' +
'in the `syncHistoryWithStore()` options. ' +
'Ensure you have added the `routerReducer` to your store\'s ' +
'reducers via `combineReducers` or whatever method you use to isolate ' +
'your reducers.'
)
}

let initialLocation
let currentLocation
let isTimeTraveling
let unsubscribeFromStore
let unsubscribeFromHistory

// What does the store say about current location?
const getLocationInStore = (useInitialIfEmpty) => {
const locationState = selectLocationState(store.getState())
return locationState.locationBeforeTransitions ||
(useInitialIfEmpty ? initialLocation : undefined)
}

// If the store is replayed, update the URL in the browser to match.
if (adjustUrlOnReplay) {
const handleStoreChange = () => {
const locationInStore = getLocationInStore(true)
if (currentLocation === locationInStore) {
return
}

// Update address bar to reflect store state
isTimeTraveling = true
currentLocation = locationInStore
history.transitionTo(Object.assign({},
locationInStore,
{ action: 'PUSH' }
))
isTimeTraveling = false
}

unsubscribeFromStore = store.subscribe(handleStoreChange)
handleStoreChange()
}

// Whenever location changes, dispatch an action to get it in the store
const handleLocationChange = (location) => {
// ... unless we just caused that location change
if (isTimeTraveling) {
return
}

// Remember where we are
currentLocation = location

// Are we being called for the first time?
if (!initialLocation) {
// Remember as a fallback in case state is reset
initialLocation = location

// Respect persisted location, if any
if (getLocationInStore()) {
return
}
}

// Tell the store to update by dispatching an action
store.dispatch({
type: LOCATION_CHANGE,
locationBeforeTransitions: location
})
}
unsubscribeFromHistory = history.listen(handleLocationChange)

// The enhanced history uses store as source of truth
return Object.assign({}, history, {
// The listeners are subscribed to the store instead of history
listen(listener) {
// History listeners expect a synchronous call
listener(getLocationInStore(true))

// Keep track of whether we unsubscribed, as Redux store
// only applies changes in subscriptions on next dispatch
let unsubscribed = false
const unsubscribeFromStore = store.subscribe(() => {
if (!unsubscribed) {
listener(getLocationInStore(true))
}
})

// Let user unsubscribe later
return () => {
unsubscribed = true
unsubscribeFromStore()
}
},

// It also provides a way to destroy internal listeners
dispose() {
if (adjustUrlOnReplay) {
unsubscribeFromStore()
}
unsubscribeFromHistory()
}
})
}

0 comments on commit 2946938

Please sign in to comment.