1
- const deepEqual = require ( 'deep-equal' ) ;
1
+ import deepEqual from 'deep-equal' ;
2
2
3
3
// Constants
4
4
5
- const UPDATE_PATH = "@@router/UPDATE_PATH" ;
5
+ export const UPDATE_PATH = "@@router/UPDATE_PATH" ;
6
+ const INIT_PATH = "@@router/INIT_PATH" ;
6
7
const SELECT_STATE = state => state . routing ;
7
8
8
- // Action creator
9
+ // Action creators
9
10
10
- function pushPath ( path , state , { avoidRouterUpdate = false } = { } ) {
11
+ function initPath ( path , state ) {
12
+ return {
13
+ type : INIT_PATH ,
14
+ payload : {
15
+ path : path ,
16
+ state : state ,
17
+ replace : false ,
18
+ avoidRouterUpdate : true
19
+ }
20
+ } ;
21
+ }
22
+
23
+ export function pushPath ( path , state , { avoidRouterUpdate = false } = { } ) {
11
24
return {
12
25
type : UPDATE_PATH ,
13
26
payload : {
@@ -19,7 +32,7 @@ function pushPath(path, state, { avoidRouterUpdate = false } = {}) {
19
32
} ;
20
33
}
21
34
22
- function replacePath ( path , state , { avoidRouterUpdate = false } = { } ) {
35
+ export function replacePath ( path , state , { avoidRouterUpdate = false } = { } ) {
23
36
return {
24
37
type : UPDATE_PATH ,
25
38
payload : {
@@ -33,15 +46,15 @@ function replacePath(path, state, { avoidRouterUpdate = false } = {}) {
33
46
34
47
// Reducer
35
48
36
- const initialState = {
49
+ let initialState = {
37
50
changeId : 1 ,
38
51
path : undefined ,
39
52
state : undefined ,
40
53
replace : false
41
54
} ;
42
55
43
56
function update ( state = initialState , { type, payload } ) {
44
- if ( type === UPDATE_PATH ) {
57
+ if ( type === INIT_PATH || type === UPDATE_PATH ) {
45
58
return Object . assign ( { } , state , {
46
59
path : payload . path ,
47
60
changeId : state . changeId + ( payload . avoidRouterUpdate ? 0 : 1 ) ,
@@ -55,12 +68,20 @@ function update(state=initialState, { type, payload }) {
55
68
// Syncing
56
69
57
70
function locationsAreEqual ( a , b ) {
58
- return a . path === b . path && deepEqual ( a . state , b . state ) ;
71
+ return a != null && b != null && a . path === b . path && deepEqual ( a . state , b . state ) ;
59
72
}
60
73
61
- function syncReduxAndRouter ( history , store , selectRouterState = SELECT_STATE ) {
74
+ export function syncReduxAndRouter ( history , store , selectRouterState = SELECT_STATE ) {
62
75
const getRouterState = ( ) => selectRouterState ( store . getState ( ) ) ;
63
- let lastChangeId = 0 ;
76
+
77
+ // To properly handle store updates we need to track the last route.
78
+ // This route contains a `changeId` which is updated on every
79
+ // `pushPath` and `replacePath`. If this id changes we always
80
+ // trigger a history update. However, if the id does not change, we
81
+ // check if the location has changed, and if it is we trigger a
82
+ // history update. It's possible for this to happen when something
83
+ // reloads the entire app state such as redux devtools.
84
+ let lastRoute = undefined ;
64
85
65
86
if ( ! getRouterState ( ) ) {
66
87
throw new Error (
@@ -75,30 +96,50 @@ function syncReduxAndRouter(history, store, selectRouterState = SELECT_STATE) {
75
96
state : location . state
76
97
} ;
77
98
78
- // Avoid dispatching an action if the store is already up-to-date,
79
- // even if `history` wouldn't do anything if the location is the same
80
- if ( locationsAreEqual ( getRouterState ( ) , route ) ) return ;
81
-
82
- const updatePath = location . action === 'REPLACE'
83
- ? replacePath
84
- : pushPath ;
85
-
86
- store . dispatch ( updatePath ( route . path , route . state , { avoidRouterUpdate : true } ) ) ;
99
+ if ( ! lastRoute ) {
100
+ // `initialState` *should* represent the current location when
101
+ // the app loads, but we cannot get the current location when it
102
+ // is defined. What happens is `history.listen` is called
103
+ // immediately when it is registered, and it updates the app
104
+ // state with an UPDATE_PATH action. This causes problem when
105
+ // users are listening to UPDATE_PATH actions just for
106
+ // *changes*, and with redux devtools because "revert" will use
107
+ // `initialState` and it won't revert to the original URL.
108
+ // Instead, we specialize the first route notification and do
109
+ // different things based on it.
110
+ initialState = {
111
+ changeId : 1 ,
112
+ path : route . path ,
113
+ state : route . state ,
114
+ replace : false
115
+ } ;
116
+
117
+ // Also set `lastRoute` so that the store subscriber doesn't
118
+ // trigger an unnecessary `pushState` on load
119
+ lastRoute = initialState ;
120
+
121
+ store . dispatch ( initPath ( route . path , route . state ) ) ;
122
+ } else if ( ! locationsAreEqual ( getRouterState ( ) , route ) ) {
123
+ // The above check avoids dispatching an action if the store is
124
+ // already up-to-date
125
+ const method = location . action === 'REPLACE' ? replacePath : pushPath ;
126
+ store . dispatch ( method ( route . path , route . state , { avoidRouterUpdate : true } ) ) ;
127
+ }
87
128
} ) ;
88
129
89
130
const unsubscribeStore = store . subscribe ( ( ) => {
90
- const routing = getRouterState ( ) ;
91
-
92
- // Only update the router once per `pushPath` call. This is
93
- // indicated by the `changeId` state; when that number changes, we
94
- // should update the history.
95
- if ( lastChangeId === routing . changeId ) return ;
131
+ let routing = getRouterState ( ) ;
96
132
97
- lastChangeId = routing . changeId ;
133
+ // Only trigger history update if this is a new change or the
134
+ // location has changed.
135
+ if ( lastRoute . changeId !== routing . changeId ||
136
+ ! locationsAreEqual ( lastRoute , routing ) ) {
98
137
99
- const method = routing . replace ? 'replaceState' : 'pushState' ;
138
+ lastRoute = routing ;
139
+ const method = routing . replace ? 'replaceState' : 'pushState' ;
140
+ history [ method ] ( routing . state , routing . path ) ;
141
+ }
100
142
101
- history [ method ] ( routing . state , routing . path ) ;
102
143
} ) ;
103
144
104
145
return function unsubscribe ( ) {
@@ -107,10 +148,4 @@ function syncReduxAndRouter(history, store, selectRouterState = SELECT_STATE) {
107
148
} ;
108
149
}
109
150
110
- module . exports = {
111
- UPDATE_PATH ,
112
- pushPath,
113
- replacePath,
114
- syncReduxAndRouter,
115
- routeReducer : update
116
- } ;
151
+ export { update as routeReducer } ;
0 commit comments