-
-
Notifications
You must be signed in to change notification settings - Fork 138
/
loader.tsx
127 lines (97 loc) · 5.01 KB
/
loader.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
import { FC, Suspense, lazy, useEffect, useState } from 'react';
import sleep from '@/utils/sleep';
import { AnyProps, LoadComponent, LoaderDefaultOptions } from './types';
// a little bit complex staff is going on here
// let me explain it
// usually, we load components asynchronously with `Suspense` and `lazy` by this way
/*
<Suspense fallback={<Loading />}>
{lazy(_ => import('path/to/the/component'))}
</Suspense>
*/
// here we have two major problems:
// 1) When the loading process is finished "quickly", we will see the fallback component
// has come-and-gone quickly, which will lead to blinking on the page
// The solution of the first problem is a so-called "delayed fallback", which gives us
// an opportunity to not show the fallback component if the loading process
// takes less than a certain amount of time
// So, the implementation of it is here:
function getDelayedFallback(Fallback: FC, delay: number) {
return function DelayedFallback(props: AnyProps) {
const [isDelayPassed, setIsDelayPassed] = useState(false);
useEffect(() => {
const timerId = setTimeout(() => setIsDelayPassed(true), delay);
return () => clearTimeout(timerId);
}, []);
return isDelayPassed ? <Fallback {...props} /> : null;
};
}
/* ================================================================================== */
// 2) The second one is the minimum amount of time of fallback render.
// We said that `DelayedFallback` will not show the fallback component in all cases
// when the loading process is finished during the `delay` amount of time,
// but when that process is continuing longer than the `delay`, then the fallback component should
// be appeared. Okay, now let's consider a situation when the loading process finishes a millisecond
// after appearing of the fallback component. We will see the fallback component has come-and-gone
// quickly, which will again lead to blinking on the page.
// The solution of the second problem is to set of a minimum timeout, which will
// ensure that the fallback component will be rendered for that minimum amount of time
const getLazyComponent = (loadComponent: LoadComponent, loaderOptions: LoaderDefaultOptions) =>
lazy(() => {
// fix the moment of starting loading
const start = performance.now();
// start loading
return loadComponent().then((moduleExports) => {
// loading is finished
const end = performance.now();
const diff = end - start;
// first of all, let's remember that we also have `loaderOptions` optionally
// provided by user, it has `delay` and `minimumLoading`:
// 1) `delay` - if the loading process is finished during this amount of time
// the user will not see the fallback component at all
// 2) `minimumLoading` - but if it appears, it will stay rendered for at least
// this amount of time
// so, according to above mentioned, there are three conditions we are interested in
// 1) when `diff` is less than `delay`; in this case, we will immediately return
// the result, thereby we will prevent the rendering of the fallback
// and the main component will be rendered
// 2) when `diff` is bigger than `delay` but less than `delay + minimumLoading`;
// it means `fallback` component has already been rendering and we have to
// wait (starting from this moment) for `delay + minimumLoading - diff`
// amount of time
// 3) when `diff` is bigger than `delay + minimumLoading`. It means we don't need to wait
// anymore and we should immediately return the result as we do it in 1) case.
// so, in the 1) and 3) cases we return the result immediately, and in 2) case we have to wait
// at least for `delay + minimumLoading - diff` amount of time
const { delay, minimumLoading } = loaderOptions;
if (diff < delay || (diff > delay && diff > delay + minimumLoading)) {
return moduleExports;
}
return sleep(delay + minimumLoading - diff).then(() => moduleExports);
});
});
/* ================================================================================== */
// And the combination of these two (plus some "magic" plus some backflips)
// will secure us from having any kind of blinking in the process of asynchronous loadings
// INFO: the usage of `asyncComponentLoader` looks like this:
// asyncComponentLoader(() => import('pages/Welcome'))
function asyncComponentLoader(
loadComponent: LoadComponent,
additionalProps: AnyProps,
loaderOptions: LoaderDefaultOptions,
FallbackWaiting: FC,
) {
const Fallback = loaderOptions.delay
? getDelayedFallback(FallbackWaiting, loaderOptions.delay)
: FallbackWaiting;
const LazyComponent = getLazyComponent(loadComponent, loaderOptions);
return function AsyncComponent(props: AnyProps) {
return (
<Suspense fallback={<Fallback />}>
<LazyComponent {...additionalProps} {...props} />
</Suspense>
);
};
}
export { getDelayedFallback };
export default asyncComponentLoader;