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
- replace : false
25
+ replace : false ,
26
+ key : undefined
39
27
}
40
28
41
- function update ( state = initialState , { type, payload } ) {
29
+ export function routeReducer ( state = initialState , { type, payload } ) {
42
30
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
49
32
}
33
+
50
34
return state
51
35
}
52
36
53
37
// 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
38
function createPath ( location ) {
60
39
const { pathname, search, hash } = location
61
40
let result = pathname
@@ -66,84 +45,75 @@ function createPath(location) {
66
45
return result
67
46
}
68
47
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
87
51
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
93
57
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
+ } )
111
61
112
- // Also set `lastRoute` so that the store subscriber doesn't
113
- // trigger an unnecessary `pushState` on load
114
- lastRoute = initialState
62
+ connected = true
115
63
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
+ }
124
69
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
+ }
127
77
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'
132
81
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 )
139
83
}
84
+ }
140
85
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
+ }
142
108
143
- return function unsubscribe ( ) {
109
+ middleware . unsubscribe = ( ) => {
144
110
unsubscribeHistory ( )
145
- unsubscribeStore ( )
111
+ if ( unsubscribeStore ) {
112
+ unsubscribeStore ( )
113
+ }
114
+
115
+ connected = false
146
116
}
147
- }
148
117
149
- export { update as routeReducer }
118
+ return middleware
119
+ }
0 commit comments