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

Commit f15c977

Browse files
committed
Merge pull request #141 from rackt/middleware
Use middleware to synchronize store to history
2 parents 56cbf5d + 30b1187 commit f15c977

File tree

4 files changed

+323
-368
lines changed

4 files changed

+323
-368
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: 78 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,112 @@
1-
import deepEqual from 'deep-equal'
2-
31
// Constants
42

5-
export const UPDATE_PATH = '@@router/UPDATE_PATH'
3+
export const TRANSITION = '@@router/TRANSITION'
4+
export const UPDATE_LOCATION = '@@router/UPDATE_LOCATION'
5+
66
const SELECT_STATE = state => state.routing
77

8-
export function pushPath(path, state, { avoidRouterUpdate = false } = {}) {
9-
return {
10-
type: UPDATE_PATH,
11-
payload: {
12-
path: path,
13-
state: state,
14-
replace: false,
15-
avoidRouterUpdate: !!avoidRouterUpdate
16-
}
17-
}
8+
function transition(method) {
9+
return arg => ({
10+
type: TRANSITION,
11+
method, arg
12+
})
1813
}
1914

20-
export function replacePath(path, state, { avoidRouterUpdate = false } = {}) {
15+
export const push = transition('push')
16+
export const replace = transition('replace')
17+
18+
// TODO: Add go, goBack, goForward.
19+
20+
function updateLocation(location) {
2121
return {
22-
type: UPDATE_PATH,
23-
payload: {
24-
path: path,
25-
state: state,
26-
replace: true,
27-
avoidRouterUpdate: !!avoidRouterUpdate
28-
}
22+
type: UPDATE_LOCATION,
23+
location
2924
}
3025
}
3126

3227
// Reducer
3328

34-
let initialState = {
35-
changeId: 1,
36-
path: undefined,
37-
state: undefined,
38-
replace: false
29+
const initialState = {
30+
location: undefined
3931
}
4032

41-
function update(state=initialState, { type, payload }) {
42-
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-
})
33+
export function routeReducer(state = initialState, { type, location }) {
34+
if (type !== UPDATE_LOCATION) {
35+
return state
4936
}
50-
return state
37+
38+
return { location }
5139
}
5240

5341
// Syncing
5442

55-
function locationsAreEqual(a, b) {
56-
return a != null && b != null && a.path === b.path && deepEqual(a.state, b.state)
57-
}
43+
export function syncHistory(history) {
44+
let unsubscribeHistory, currentKey, unsubscribeStore
45+
let connected = false, syncing = false
5846

59-
function createPath(location) {
60-
const { pathname, search, hash } = location
61-
let result = pathname
62-
if (search)
63-
result += search
64-
if (hash)
65-
result += hash
66-
return result
67-
}
47+
function middleware(store) {
48+
unsubscribeHistory = history.listen(location => {
49+
currentKey = location.key
50+
if (syncing) {
51+
// Don't dispatch a new action if we're replaying location.
52+
return
53+
}
6854

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-
}
55+
store.dispatch(updateLocation(location))
56+
})
8757

88-
const unsubscribeHistory = history.listen(location => {
89-
const route = {
90-
path: createPath(location),
91-
state: location.state
92-
}
58+
connected = true
9359

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
60+
return next => action => {
61+
if (action.type !== TRANSITION || !connected) {
62+
next(action)
63+
return
11064
}
11165

112-
// Also set `lastRoute` so that the store subscriber doesn't
113-
// trigger an unnecessary `pushState` on load
114-
lastRoute = initialState
66+
// FIXME: Is it correct to swallow the TRANSITION action here and replace
67+
// it with UPDATE_LOCATION instead? We could also use the same type in
68+
// both places instead and just set the location on the action.
11569

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 }))
70+
const { method, arg } = action
71+
history[method](arg)
12272
}
123-
})
124-
125-
const unsubscribeStore = store.subscribe(() => {
126-
let routing = getRouterState()
127-
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)) {
73+
}
13274

133-
lastRoute = routing
134-
const method = routing.replace ? 'replace' : 'push'
135-
history[method]({
136-
pathname: routing.path,
137-
state: routing.state
75+
middleware.syncHistoryToStore =
76+
(store, selectRouterState = SELECT_STATE) => {
77+
const getRouterState = () => selectRouterState(store.getState())
78+
const { location: initialLocation } = getRouterState()
79+
80+
unsubscribeStore = store.subscribe(() => {
81+
const { location } = getRouterState()
82+
83+
// If we're resetting to the beginning, use the saved initial value. We
84+
// need to dispatch a new action at this point to populate the store
85+
// appropriately.
86+
if (!location) {
87+
history.transitionTo(initialLocation)
88+
return
89+
}
90+
91+
// Otherwise, if we need to update the history location, do so without
92+
// dispatching a new action, as we're just bringing history in sync
93+
// with the store.
94+
if (location.key !== currentKey) {
95+
syncing = true
96+
history.transitionTo(location)
97+
syncing = false
98+
}
13899
})
139100
}
140101

141-
})
142-
143-
return function unsubscribe() {
102+
middleware.unsubscribe = () => {
144103
unsubscribeHistory()
145-
unsubscribeStore()
104+
if (unsubscribeStore) {
105+
unsubscribeStore()
106+
}
107+
108+
connected = false
146109
}
147-
}
148110

149-
export { update as routeReducer }
111+
return middleware
112+
}

0 commit comments

Comments
 (0)