-
Notifications
You must be signed in to change notification settings - Fork 30
/
ConnectedRouter.ts
113 lines (99 loc) · 4.19 KB
/
ConnectedRouter.ts
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
import React, { useEffect, useRef } from 'react'
import { useStore } from 'react-redux'
import NextRouter, { SingletonRouter } from 'next/router'
import { onLocationChanged } from './actions'
import patchRouter from './patchRouter'
import locationFromUrl from './utils/locationFromUrl'
import { Structure, RouterAction, LocationState } from './types'
type ConnectedRouterProps = {
children?: React.ReactNode;
reducerKey?: string;
Router?: SingletonRouter;
}
const createConnectedRouter = (structure: Structure): React.FC<ConnectedRouterProps> => {
const { getIn } = structure
/*
* ConnectedRouter listens to Next Router events.
* When history is changed, it dispatches an action
* to update router state in redux store.
*/
const ConnectedRouter: React.FC<ConnectedRouterProps> = props => {
const Router = props.Router || NextRouter
const { reducerKey = 'router' } = props
const store = useStore()
const ongoingRouteChanges = useRef(0)
const isTimeTravelEnabled = useRef(false)
const inTimeTravelling = useRef(false)
function trackRouteComplete(): void {
isTimeTravelEnabled.current = --ongoingRouteChanges.current <= 0
}
function trackRouteStart(): void {
isTimeTravelEnabled.current = ++ongoingRouteChanges.current <= 0
}
useEffect(() => {
function listenStoreChanges(): void {
/**
* Next.js asynchronously loads routes, and Redux actions can be
* dispatched during this process before Router's history change.
* To prevent time travel changes during it, time travel detection
* is disabled when Router change starts, and later enabled on change
* completion or error.
*/
if (!isTimeTravelEnabled.current) {
return
}
const storeLocation = getIn(store.getState(), [reducerKey, 'location']) as LocationState
const {
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
href
} = storeLocation
// Extract Router's location
const historyLocation = locationFromUrl(Router.asPath)
const { pathname: pathnameInHistory, search: searchInHistory, hash: hashInHistory } = historyLocation
// If we do time travelling, the location in store is changed but location in Router is not changed
const locationMismatch =
pathnameInHistory !== pathnameInStore || searchInHistory !== searchInStore || hashInStore !== hashInHistory
if (locationMismatch) {
const as = `${pathnameInStore}${searchInStore}${hashInStore}`
// Update Router's location to match store's location
inTimeTravelling.current = true
Router.replace(href, as)
}
}
const unsubscribeStore = store.subscribe(listenStoreChanges)
return unsubscribeStore
}, [Router, store, reducerKey])
useEffect(() => {
let unpatchRouter = (): void => {}
function listenRouteChanges(url: string, as: string, action: RouterAction): void {
// Dispatch onLocationChanged except when we're time travelling
if (!inTimeTravelling.current) {
store.dispatch(onLocationChanged(locationFromUrl(url, as), action))
} else {
inTimeTravelling.current = false
}
}
Router.ready(() => {
// Router.ready ensures that Router.router is defined
// @ts-ignore
unpatchRouter = patchRouter(Router)
Router.events.on('routeChangeStart', trackRouteStart)
Router.events.on('routeChangeError', trackRouteComplete)
Router.events.on('routeChangeComplete', trackRouteComplete)
Router.events.on('connectedRouteChangeComplete', listenRouteChanges)
})
return () => {
unpatchRouter()
Router.events.off('routeChangeStart', trackRouteStart)
Router.events.off('routeChangeError', trackRouteComplete)
Router.events.off('routeChangeComplete', trackRouteComplete)
Router.events.off('connectedRouteChangeComplete', listenRouteChanges)
}
}, [Router, reducerKey, store])
return React.createElement(React.Fragment, {}, props.children)
}
return ConnectedRouter
}
export default createConnectedRouter