/
HibernatingSwitch.tsx
208 lines (183 loc) · 6.79 KB
/
HibernatingSwitch.tsx
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import { LimitedCache } from 'limited-cache';
import { ReactComponentLike } from 'prop-types';
import React, { ReactNode, useMemo } from 'react';
import { isElement } from 'react-is';
import { createHtmlPortalNode, InPortal, OutPortal, HtmlPortalNode } from 'react-reverse-portal';
import {
Redirect,
Route,
RouteComponentProps,
RouteProps,
Switch,
SwitchProps,
} from 'react-router';
import HibernatingRoute from './HibernatingRoute';
import renderRoute from './renderRoute';
interface HibernatingSwitchProps extends SwitchProps {
children: ReactNode;
maxCacheSize?: number;
maxCacheTime?: number;
WrapperComponent?: ReactComponentLike | null;
}
type PortalRecord = {
portalNode: HtmlPortalNode;
routerProps: RouteComponentProps;
routeProps: RouteProps;
};
const HibernatingSwitch: React.FC<HibernatingSwitchProps> = ({
children,
maxCacheSize,
maxCacheTime,
WrapperComponent,
...allOtherProps
}: HibernatingSwitchProps) => {
const portalRecordCache = useMemo(
() =>
LimitedCache({
maxCacheSize,
maxCacheTime,
}),
[],
);
const currentPathKeyRef = React.useRef<string>();
const currentPathKey = currentPathKeyRef.current;
const [currentPortalRecord, setCurrentPortalRecord] = React.useState<PortalRecord | null>(null);
if (process.env.NODE_ENV !== 'production') {
const InitialWrapperComponentRef = React.useRef(WrapperComponent);
if (WrapperComponent !== InitialWrapperComponentRef.current) {
console.warn(
'The WrapperComponent component changed between renders: this will cause a remount',
);
InitialWrapperComponentRef.current = WrapperComponent;
}
}
const activatePortalForComponent = (
routerProps: RouteComponentProps,
routeProps: RouteProps,
): void => {
// We don't really care about the *path* that was matched. Instead, we care about the *url* that activated us
const pathKey = routerProps.match.url;
if (pathKey === currentPathKey) {
// Nothing has really changed: just update any props
(currentPortalRecord as PortalRecord).routerProps = routerProps;
(currentPortalRecord as PortalRecord).routeProps = routeProps;
} else {
// New route! Stash the old and move to the new
// But first, pull any previous portal record for the 'new' screen (in case it's in the cache) --
// otherwise it might get removed when we add the old portal record, if the cache size is small.
const previousPortalRecord = portalRecordCache.get(pathKey);
if (currentPathKey) {
portalRecordCache.set(currentPathKey, currentPortalRecord);
}
let newPortalRecord;
if (previousPortalRecord) {
// Reactivate the prior subtree
previousPortalRecord.routerProps = routerProps;
previousPortalRecord.routeProps = routeProps;
newPortalRecord = previousPortalRecord;
} else {
// Make a new portal for the new subtree
newPortalRecord = {
portalNode: createHtmlPortalNode(),
routerProps,
routeProps,
};
}
currentPathKeyRef.current = pathKey;
setCurrentPortalRecord(newPortalRecord);
}
};
const deactivatePortal = (): void => {
if (currentPathKey && currentPortalRecord) {
portalRecordCache.set(currentPathKey, currentPortalRecord);
// We need to unmount the old OutPortal *immediately* so that it can swap in its original dom node,
// or else React hits an error trying to unmount in the incorrect node
currentPortalRecord.portalNode.unmount();
currentPathKeyRef.current = '';
setCurrentPortalRecord(null);
}
};
const childrenWithHibernation = React.Children.map(children, (child) => {
if (isElement(child)) {
const {
type,
props: routeProps,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
props: { children, component, isHibernatingRoute, render, ...allOtherRouteProps },
} = child;
if (type === HibernatingRoute || isHibernatingRoute) {
// Replace it: it will activate the remote node when the route matches
return (
<Route
{...allOtherRouteProps}
render={(routerProps): null => {
activatePortalForComponent(routerProps, routeProps);
return null;
}}
/>
);
}
if (type !== Redirect && isHibernatingRoute !== false) {
// Every child should either be a Hibernating Route, vanilla Route, or Redirect -- and if it's neither a
// vanilla route nor a Hibernating Route, it's probably a custom wrapper around Route.
// If it's any non-Hibernating route Route then we need to deactivate any portal which might be active from
// a Hibernating Route. But no matter what it might be, we want to deactivate before rendering, to be safe.
return (
<Route
{...allOtherRouteProps}
render={(routerProps: RouteComponentProps): ReactNode => {
deactivatePortal();
return renderRoute(routerProps, routeProps);
}}
/>
);
}
}
// If it's a Redirect, or anything we can't recognize, let it pass through as-is
return child;
});
const portalRecordCacheFull = portalRecordCache.getAll();
const allPortalKeys: Array<string> = Object.keys(portalRecordCacheFull);
if (currentPathKey && !allPortalKeys.includes(currentPathKey)) {
allPortalKeys.push(currentPathKey);
}
return (
<React.Fragment>
<Switch {...allOtherProps}>{childrenWithHibernation}</Switch>
<React.Fragment>
{allPortalKeys.map((pathKey) => {
const portalRecord =
pathKey === currentPathKey ? currentPortalRecord : portalRecordCacheFull[pathKey];
if (!portalRecord) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`portalRecord is missing for pathKey "${pathKey}"`);
}
return null;
}
const { portalNode, routerProps, routeProps } = portalRecord;
const routeContent = renderRoute(routerProps, routeProps);
const wrappedRouteContent = WrapperComponent ? (
<WrapperComponent shouldUpdate={pathKey === currentPathKey}>
{routeContent}
</WrapperComponent>
) : (
routeContent
);
return (
<InPortal key={pathKey} node={portalNode}>
{wrappedRouteContent}
</InPortal>
);
})}
{!!currentPortalRecord && <OutPortal node={currentPortalRecord.portalNode} />}
</React.Fragment>
</React.Fragment>
);
};
HibernatingSwitch.defaultProps = {
maxCacheSize: 5,
maxCacheTime: 5 * 60 * 1000,
WrapperComponent: null,
};
export default HibernatingSwitch;
export { HibernatingSwitchProps };