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

Commit a0c2a66

Browse files
committed
Use middleware to synchronize store to history
1 parent a668989 commit a0c2a66

File tree

5 files changed

+151
-205
lines changed

5 files changed

+151
-205
lines changed

.babelrc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
2-
"presets": ["es2015"],
3-
"plugins": ["transform-object-assign"]
2+
"presets": ["es2015", "stage-2"]
43
}

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
"babel-core": "^6.2.1",
4343
"babel-eslint": "^4.1.6",
4444
"babel-loader": "^6.2.0",
45-
"babel-plugin-transform-object-assign": "^6.0.14",
4645
"babel-preset-es2015": "^6.1.2",
46+
"babel-preset-stage-2": "^6.3.13",
4747
"eslint": "^1.10.3",
4848
"eslint-config-rackt": "^1.1.1",
4949
"expect": "^1.13.0",
@@ -64,8 +64,5 @@
6464
"redux": "^3.0.4",
6565
"redux-devtools": "^2.1.5",
6666
"webpack": "^1.12.9"
67-
},
68-
"dependencies": {
69-
"deep-equal": "^1.0.1"
7067
}
7168
}

src/index.js

Lines changed: 68 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,40 @@
1-
import deepEqual from 'deep-equal'
2-
31
// Constants
42

53
export const UPDATE_PATH = '@@router/UPDATE_PATH'
64
const SELECT_STATE = state => state.routing
75

8-
export function pushPath(path, state, { avoidRouterUpdate = false } = {}) {
6+
export function pushPath(path, state, key) {
97
return {
108
type: UPDATE_PATH,
11-
payload: {
12-
path: path,
13-
state: state,
14-
replace: false,
15-
avoidRouterUpdate: !!avoidRouterUpdate
16-
}
9+
payload: { path, state, key, replace: false }
1710
}
1811
}
1912

20-
export function replacePath(path, state, { avoidRouterUpdate = false } = {}) {
13+
export function replacePath(path, state, key) {
2114
return {
2215
type: UPDATE_PATH,
23-
payload: {
24-
path: path,
25-
state: state,
26-
replace: true,
27-
avoidRouterUpdate: !!avoidRouterUpdate
28-
}
16+
payload: { path, state, key, replace: true }
2917
}
3018
}
3119

3220
// Reducer
3321

3422
let initialState = {
35-
changeId: 1,
3623
path: undefined,
3724
state: undefined,
38-
replace: false
25+
replace: false,
26+
key: undefined
3927
}
4028

41-
function update(state=initialState, { type, payload }) {
29+
export function routeReducer(state=initialState, { type, payload }) {
4230
if(type === UPDATE_PATH) {
43-
return Object.assign({}, state, {
44-
path: payload.path,
45-
changeId: state.changeId + (payload.avoidRouterUpdate ? 0 : 1),
46-
state: payload.state,
47-
replace: payload.replace
48-
})
31+
return payload
4932
}
33+
5034
return state
5135
}
5236

5337
// Syncing
54-
55-
function locationsAreEqual(a, b) {
56-
return a != null && b != null && a.path === b.path && deepEqual(a.state, b.state)
57-
}
58-
5938
function createPath(location) {
6039
const { pathname, search, hash } = location
6140
let result = pathname
@@ -66,84 +45,75 @@ function createPath(location) {
6645
return result
6746
}
6847

69-
export function syncReduxAndRouter(history, store, selectRouterState = SELECT_STATE) {
70-
const getRouterState = () => selectRouterState(store.getState())
71-
72-
// To properly handle store updates we need to track the last route.
73-
// This route contains a `changeId` which is updated on every
74-
// `pushPath` and `replacePath`. If this id changes we always
75-
// trigger a history update. However, if the id does not change, we
76-
// check if the location has changed, and if it is we trigger a
77-
// history update. It's possible for this to happen when something
78-
// reloads the entire app state such as redux devtools.
79-
let lastRoute = undefined
80-
81-
if(!getRouterState()) {
82-
throw new Error(
83-
'Cannot sync router: route state does not exist (`state.routing` by default). ' +
84-
'Did you install the routing reducer?'
85-
)
86-
}
48+
export function syncHistory(history) {
49+
let unsubscribeHistory, currentKey, unsubscribeStore
50+
let connected = false
8751

88-
const unsubscribeHistory = history.listen(location => {
89-
const route = {
90-
path: createPath(location),
91-
state: location.state
92-
}
52+
function middleware(store) {
53+
unsubscribeHistory = history.listen(location => {
54+
const path = createPath(location)
55+
const { state, key } = location
56+
currentKey = key
9357

94-
if (!lastRoute) {
95-
// `initialState` *should* represent the current location when
96-
// the app loads, but we cannot get the current location when it
97-
// is defined. What happens is `history.listen` is called
98-
// immediately when it is registered, and it updates the app
99-
// state with an UPDATE_PATH action. This causes problem when
100-
// users are listening to UPDATE_PATH actions just for
101-
// *changes*, and with redux devtools because "revert" will use
102-
// `initialState` and it won't revert to the original URL.
103-
// Instead, we specialize the first route notification and do
104-
// different things based on it.
105-
initialState = {
106-
changeId: 1,
107-
path: route.path,
108-
state: route.state,
109-
replace: false
110-
}
58+
const method = location.action === 'REPLACE' ? replacePath : pushPath
59+
store.dispatch(method(path, state, key))
60+
})
11161

112-
// Also set `lastRoute` so that the store subscriber doesn't
113-
// trigger an unnecessary `pushState` on load
114-
lastRoute = initialState
62+
connected = true
11563

116-
store.dispatch(pushPath(route.path, route.state, { avoidRouterUpdate: true }));
117-
} else if(!locationsAreEqual(getRouterState(), route)) {
118-
// The above check avoids dispatching an action if the store is
119-
// already up-to-date
120-
const method = location.action === 'REPLACE' ? replacePath : pushPath
121-
store.dispatch(method(route.path, route.state, { avoidRouterUpdate: true }))
122-
}
123-
})
64+
return next => action => {
65+
if (action.type !== UPDATE_PATH) {
66+
next(action)
67+
return
68+
}
12469

125-
const unsubscribeStore = store.subscribe(() => {
126-
let routing = getRouterState()
70+
const { payload } = action
71+
if (payload.key || !connected) {
72+
// Either this came from the history, or else we're not forwarding
73+
// location actions to history.
74+
next(action)
75+
return
76+
}
12777

128-
// Only trigger history update if this is a new change or the
129-
// location has changed.
130-
if(lastRoute.changeId !== routing.changeId ||
131-
!locationsAreEqual(lastRoute, routing)) {
78+
const { replace, state, path } = payload
79+
// FIXME: ???! `path` and `pathname` are _not_ synonymous.
80+
const method = replace ? 'replaceState' : 'pushState'
13281

133-
lastRoute = routing
134-
const method = routing.replace ? 'replace' : 'push'
135-
history[method]({
136-
pathname: routing.path,
137-
state: routing.state
138-
})
82+
history[method](state, path)
13983
}
84+
}
14085

141-
})
86+
middleware.syncHistoryToStore =
87+
(store, selectRouterState = SELECT_STATE) => {
88+
const getRouterState = () => selectRouterState(store.getState())
89+
const {
90+
key: initialKey, state: initialState, path: initialPath
91+
} = getRouterState()
92+
93+
unsubscribeStore = store.subscribe(() => {
94+
let { key, state, path } = getRouterState()
95+
96+
// If we're resetting to the beginning, use the saved values.
97+
if (key === undefined) {
98+
key = initialKey
99+
state = initialState
100+
path = initialPath
101+
}
102+
103+
if (key !== currentKey) {
104+
history.pushState(state, path)
105+
}
106+
})
107+
}
142108

143-
return function unsubscribe() {
109+
middleware.unsubscribe = () => {
144110
unsubscribeHistory()
145-
unsubscribeStore()
111+
if (unsubscribeStore) {
112+
unsubscribeStore()
113+
}
114+
115+
connected = false
146116
}
147-
}
148117

149-
export { update as routeReducer }
118+
return middleware
119+
}

test/browser/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ import { createHashHistory, createHistory } from 'history'
22
import createTests from '../createTests.js'
33

44
createTests(createHashHistory, 'Hash History', () => window.location = '#/')
5-
createTests(createHistory, 'Browser History', () => window.history.replaceState(null, null, '/'))
5+
createTests(createHistory, 'Browser History', () => window.history.replaceState(null, null, '/'), true)

0 commit comments

Comments
 (0)