-
Notifications
You must be signed in to change notification settings - Fork 219
/
hooks.ts
121 lines (107 loc) · 3.38 KB
/
hooks.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
114
115
116
117
118
119
120
121
import React from 'react';
import {useIntersection} from '@shopify/react-intersection-observer';
import {
DeferTiming,
WindowWithRequestIdleCallback,
RequestIdleCallbackHandle,
} from '@shopify/async';
import {useMountedRef} from '@shopify/react-hooks';
import load from './load';
interface Options<Imported> {
nonce?: string;
defer?: DeferTiming;
getImport(window: Window): Imported;
}
export enum Status {
Initial = 'Initial',
Failed = 'Failed',
Complete = 'Complete',
Loading = 'Loading',
}
type Result<Imported = unknown> =
| {status: Status.Initial}
| {status: Status.Loading}
| {status: Status.Failed; error: Error}
| {status: Status.Complete; imported: Imported};
export function useImportRemote<Imported = unknown>(
source: string,
options: Options<Imported>,
): {
result: Result<Imported>;
intersectionRef: React.Ref<HTMLElement | null>;
} {
const {defer = DeferTiming.Mount, nonce = '', getImport} = options;
const [result, setResult] = React.useState<Result<Imported>>({
status: Status.Initial,
});
const idleCallbackHandle = React.useRef<RequestIdleCallbackHandle | null>(
null,
);
const mounted = useMountedRef();
const deferOption = React.useRef(defer);
if (deferOption.current !== defer) {
throw new Error(
[
'You’ve changed the defer strategy on an <ImportRemote />',
'component after it has mounted. This is not supported.',
].join(' '),
);
}
let intersection: IntersectionObserverEntry | null = null;
let intersectionRef: React.Ref<HTMLElement | null> = null;
// Normally this would be dangerous but because we are
// guaranteed to have thrown if the defer option changes
// we can be confident that a given use of this hook
// will only ever hit one of these two cases.
/* eslint-disable react-hooks/rules-of-hooks */
if (defer === DeferTiming.InViewport) {
[intersection, intersectionRef] = useIntersection();
}
/* eslint-enable react-hooks/rules-of-hooks */
const loadRemote = React.useCallback(async () => {
try {
setResult({status: Status.Loading});
const importResult = await load(source, getImport, nonce);
if (mounted.current) {
setResult({status: Status.Complete, imported: importResult});
}
} catch (error) {
if (mounted.current) {
setResult({status: Status.Failed, error});
}
}
}, [getImport, mounted, nonce, source]);
React.useEffect(() => {
if (
result.status === Status.Initial &&
defer === DeferTiming.InViewport &&
intersection &&
intersection.isIntersecting
) {
loadRemote();
}
}, [result, defer, intersection, loadRemote]);
React.useEffect(() => {
if (defer === DeferTiming.Idle) {
if ('requestIdleCallback' in window) {
idleCallbackHandle.current = (window as WindowWithRequestIdleCallback).requestIdleCallback(
loadRemote,
);
} else {
loadRemote();
}
} else if (defer === DeferTiming.Mount) {
loadRemote();
}
return () => {
if (
idleCallbackHandle.current != null &&
typeof (window as any).cancelIdleCallback === 'function'
) {
(window as any).cancelIdleCallback(idleCallbackHandle.current);
idleCallbackHandle.current = null;
}
};
}, [defer, loadRemote, intersection, nonce, getImport, source]);
return {result, intersectionRef};
}