UNPKG

3.53 kBPlain TextView Raw
1import React from 'react';
2import {ExtendedWindow} from '@shopify/useful-types';
3import {useIntersection} from '@shopify/react-intersection-observer';
4import {
5 DeferTiming,
6 WindowWithRequestIdleCallback,
7 RequestIdleCallbackHandle,
8} from '@shopify/async';
9import {useMountedRef} from '@shopify/react-hooks';
10
11import load from './load';
12
13interface Options<Imported> {
14 nonce?: string;
15 defer?: DeferTiming;
16 getImport(window: Window): Imported;
17}
18
19export enum Status {
20 Initial = 'Initial',
21 Failed = 'Failed',
22 Complete = 'Complete',
23 Loading = 'Loading',
24}
25
26type Result<Imported = unknown> =
27 | {status: Status.Initial}
28 | {status: Status.Loading}
29 | {status: Status.Failed; error: Error}
30 | {status: Status.Complete; imported: Imported};
31
32export function useImportRemote<Imported = unknown>(
33 source: string,
34 options: Options<Imported>,
35): {
36 result: Result<Imported>;
37 intersectionRef: React.Ref<HTMLElement | null>;
38} {
39 const {defer = DeferTiming.Mount, nonce = '', getImport} = options;
40 const [result, setResult] = React.useState<Result<Imported>>({
41 status: Status.Initial,
42 });
43 const idleCallbackHandle = React.useRef<RequestIdleCallbackHandle | null>(
44 null,
45 );
46 const mounted = useMountedRef();
47
48 const deferOption = React.useRef(defer);
49
50 if (deferOption.current !== defer) {
51 throw new Error(
52 [
53 'You’ve changed the defer strategy on an <ImportRemote />',
54 'component after it has mounted. This is not supported.',
55 ].join(' '),
56 );
57 }
58
59 let intersection: IntersectionObserverEntry | null = null;
60 let intersectionRef: React.Ref<HTMLElement | null> = null;
61
62 // Normally this would be dangerous but because we are
63 // guaranteed to have thrown if the defer option changes
64 // we can be confident that a given use of this hook
65 // will only ever hit one of these two cases.
66 /* eslint-disable react-hooks/rules-of-hooks */
67 if (defer === DeferTiming.InViewport) {
68 [intersection, intersectionRef] = useIntersection();
69 }
70 /* eslint-enable react-hooks/rules-of-hooks */
71
72 const loadRemote = React.useCallback(async () => {
73 try {
74 setResult({status: Status.Loading});
75 const importResult = await load(source, getImport, nonce);
76
77 if (mounted.current) {
78 setResult({status: Status.Complete, imported: importResult});
79 }
80 } catch (error) {
81 if (mounted.current) {
82 setResult({status: Status.Failed, error});
83 }
84 }
85 }, [getImport, mounted, nonce, source]);
86
87 React.useEffect(() => {
88 if (
89 result.status === Status.Initial &&
90 defer === DeferTiming.InViewport &&
91 intersection &&
92 intersection.isIntersecting
93 ) {
94 loadRemote();
95 }
96 }, [result, defer, intersection, loadRemote]);
97
98 React.useEffect(() => {
99 if (defer === DeferTiming.Idle) {
100 if ('requestIdleCallback' in window) {
101 idleCallbackHandle.current = (window as ExtendedWindow<
102 WindowWithRequestIdleCallback
103 >).requestIdleCallback(loadRemote);
104 } else {
105 loadRemote();
106 }
107 } else if (defer === DeferTiming.Mount) {
108 loadRemote();
109 }
110
111 return () => {
112 if (
113 idleCallbackHandle.current != null &&
114 typeof (window as any).cancelIdleCallback === 'function'
115 ) {
116 (window as any).cancelIdleCallback(idleCallbackHandle.current);
117 idleCallbackHandle.current = null;
118 }
119 };
120 }, [defer, loadRemote, intersection, nonce, getImport, source]);
121
122 return {result, intersectionRef};
123}