-
Notifications
You must be signed in to change notification settings - Fork 594
/
ConnectedRouter.js
159 lines (136 loc) · 5.3 KB
/
ConnectedRouter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { connect, ReactReduxContext } from 'react-redux'
import { Router } from 'react-router'
import isEqualWith from 'lodash.isequalwith'
import { onLocationChanged } from './actions'
import createSelectors from './selectors'
const createConnectedRouter = (structure) => {
const { getLocation } = createSelectors(structure)
/*
* ConnectedRouter listens to a history object passed from props.
* When history is changed, it dispatches action to redux store.
* Then, store will pass props to component to render.
* This creates uni-directional flow from history->store->router->components.
*/
class ConnectedRouter extends PureComponent {
constructor(props) {
super(props)
const { store, history, onLocationChanged, stateCompareFunction } = props
this.inTimeTravelling = false
// Subscribe to store changes to check if we are in time travelling
this.unsubscribe = store.subscribe(() => {
// Allow time travel debugging compatibility to be turned off
// as the detection for this (below) is error prone in apps where the
// store may be unmounted, a navigation occurs, and then the store is re-mounted
// during the app's lifetime. Detection could be much improved if Redux DevTools
// simply set a global variable like `REDUX_DEVTOOLS_IS_TIME_TRAVELLING=true`.
const isTimeTravelDebuggingAllowed = !props.noTimeTravelDebugging
// Extract store's location
const {
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
state: stateInStore,
} = getLocation(store.getState())
// Extract history's location
const {
pathname: pathnameInHistory,
search: searchInHistory,
hash: hashInHistory,
state: stateInHistory,
} = history.location
// If we do time travelling, the location in store is changed but location in history is not changed
if (
isTimeTravelDebuggingAllowed &&
props.history.action === 'PUSH' &&
(pathnameInHistory !== pathnameInStore ||
searchInHistory !== searchInStore ||
hashInHistory !== hashInStore ||
!isEqualWith(stateInStore, stateInHistory, stateCompareFunction))
) {
this.inTimeTravelling = true
// Update history's location to match store's location
history.push({
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
state: stateInStore,
})
}
})
const handleLocationChange = (location, action, isFirstRendering = false) => {
// Dispatch onLocationChanged except when we're in time travelling
if (!this.inTimeTravelling) {
onLocationChanged(location, action, isFirstRendering)
} else {
this.inTimeTravelling = false
}
}
// Listen to history changes
this.unlisten = history.listen(handleLocationChange)
if (!props.noInitialPop) {
// Dispatch a location change action for the initial location.
// This makes it backward-compatible with react-router-redux.
// But, we add `isFirstRendering` to `true` to prevent double-rendering.
handleLocationChange(history.location, history.action, true)
}
}
componentWillUnmount() {
this.unlisten()
this.unsubscribe()
}
render() {
const { omitRouter, history, children } = this.props
// The `omitRouter` option is available for applications that must
// have a Router instance higher in the component tree but still desire
// to use connected-react-router for its Redux integration.
if (omitRouter) {
return <>{ children }</>
}
return (
<Router history={history}>
{ children }
</Router>
)
}
}
ConnectedRouter.propTypes = {
store: PropTypes.shape({
getState: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
}).isRequired,
history: PropTypes.shape({
action: PropTypes.string.isRequired,
listen: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
basename: PropTypes.string,
children: PropTypes.oneOfType([ PropTypes.func, PropTypes.node ]),
onLocationChanged: PropTypes.func.isRequired,
noInitialPop: PropTypes.bool,
noTimeTravelDebugging: PropTypes.bool,
stateCompareFunction: PropTypes.func,
omitRouter: PropTypes.bool,
}
const mapDispatchToProps = dispatch => ({
onLocationChanged: (location, action, isFirstRendering) => dispatch(onLocationChanged(location, action, isFirstRendering))
})
const ConnectedRouterWithContext = props => {
const Context = props.context || ReactReduxContext
if (Context == null) {
throw 'Please upgrade to react-redux v6'
}
return (
<Context.Consumer>
{({ store }) => <ConnectedRouter store={store} {...props} />}
</Context.Consumer>
)
}
ConnectedRouterWithContext.propTypes = {
context: PropTypes.object,
}
return connect(null, mapDispatchToProps)(ConnectedRouterWithContext)
}
export default createConnectedRouter