1
- import deepEqual from 'deep-equal'
2
-
3
1
// Constants
4
2
5
3
export const UPDATE_PATH = '@@router/UPDATE_PATH'
6
4
const SELECT_STATE = state => state . routing
7
5
8
- export function pushPath ( path , state , { avoidRouterUpdate = false } = { } ) {
6
+ export function pushPath ( path , state , key ) {
9
7
return {
10
8
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 }
17
10
}
18
11
}
19
12
20
- export function replacePath ( path , state , { avoidRouterUpdate = false } = { } ) {
13
+ export function replacePath ( path , state , key ) {
21
14
return {
22
15
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 }
29
17
}
30
18
}
31
19
32
20
// Reducer
33
21
34
22
let initialState = {
35
- changeId : 1 ,
36
23
path : undefined ,
37
24
state : undefined ,
38
25
replace : false
39
26
}
40
27
41
- function update ( state = initialState , { type, payload } ) {
28
+ export function routeReducer ( state = initialState , { type, payload } ) {
42
29
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
- } )
30
+ return payload
49
31
}
32
+
50
33
return state
51
34
}
52
35
53
36
// Syncing
54
-
55
- function locationsAreEqual ( a , b ) {
56
- return a != null && b != null && a . path === b . path && deepEqual ( a . state , b . state )
57
- }
58
-
59
37
function createPath ( location ) {
60
38
const { pathname, search, hash } = location
61
39
let result = pathname
@@ -66,84 +44,75 @@ function createPath(location) {
66
44
return result
67
45
}
68
46
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
- }
47
+ export function syncHistory ( history ) {
48
+ let unsubscribeHistory , currentKey , unsubscribeStore
49
+ let connected = false
87
50
88
- const unsubscribeHistory = history . listen ( location => {
89
- const route = {
90
- path : createPath ( location ) ,
91
- state : location . state
92
- }
51
+ function middleware ( store ) {
52
+ unsubscribeHistory = history . listen ( location => {
53
+ const path = createPath ( location )
54
+ const { state, key } = location
55
+ currentKey = key
93
56
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
- }
57
+ const method = location . action === 'REPLACE' ? replacePath : pushPath
58
+ store . dispatch ( method ( path , state , key ) )
59
+ } )
111
60
112
- // Also set `lastRoute` so that the store subscriber doesn't
113
- // trigger an unnecessary `pushState` on load
114
- lastRoute = initialState
61
+ connected = true
115
62
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
- } )
63
+ return next => action => {
64
+ if ( action . type !== UPDATE_PATH ) {
65
+ next ( action )
66
+ return
67
+ }
124
68
125
- const unsubscribeStore = store . subscribe ( ( ) => {
126
- let routing = getRouterState ( )
69
+ const { payload } = action
70
+ if ( payload . key || ! connected ) {
71
+ // Either this came from the history, or else we're not forwarding
72
+ // location actions to history.
73
+ next ( action )
74
+ return
75
+ }
127
76
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 ) ) {
77
+ const { replace, state, path } = payload
78
+ // FIXME: ???! `path` and `pathname` are _not_ synonymous.
79
+ const method = replace ? 'replaceState' : 'pushState'
132
80
133
- lastRoute = routing
134
- const method = routing . replace ? 'replace' : 'push'
135
- history [ method ] ( {
136
- pathname : routing . path ,
137
- state : routing . state
138
- } )
81
+ history [ method ] ( state , path )
139
82
}
83
+ }
140
84
141
- } )
85
+ middleware . syncHistoryToStore =
86
+ ( store , selectRouterState = SELECT_STATE ) => {
87
+ const getRouterState = ( ) => selectRouterState ( store . getState ( ) )
88
+ const {
89
+ key : initialKey , state : initialState , path : initialPath
90
+ } = getRouterState ( )
91
+
92
+ unsubscribeStore = store . subscribe ( ( ) => {
93
+ let { key, state, path } = getRouterState ( )
94
+
95
+ // If we're resetting to the beginning, use the saved values.
96
+ if ( key === undefined ) {
97
+ key = initialKey
98
+ state = initialState
99
+ path = initialPath
100
+ }
101
+
102
+ if ( key !== currentKey ) {
103
+ history . pushState ( state , path )
104
+ }
105
+ } )
106
+ }
142
107
143
- return function unsubscribe ( ) {
108
+ middleware . unsubscribe = ( ) => {
144
109
unsubscribeHistory ( )
145
- unsubscribeStore ( )
110
+ if ( unsubscribeStore ) {
111
+ unsubscribeStore ( )
112
+ }
113
+
114
+ connected = false
146
115
}
147
- }
148
116
149
- export { update as routeReducer }
117
+ return middleware
118
+ }
0 commit comments