1 | import React from 'react';
|
2 | import {ExtendedWindow} from '@shopify/useful-types';
|
3 | import {useIntersection} from '@shopify/react-intersection-observer';
|
4 | import {
|
5 | DeferTiming,
|
6 | WindowWithRequestIdleCallback,
|
7 | RequestIdleCallbackHandle,
|
8 | } from '@shopify/async';
|
9 | import {useMountedRef} from '@shopify/react-hooks';
|
10 |
|
11 | import load from './load';
|
12 |
|
13 | interface Options<Imported> {
|
14 | nonce?: string;
|
15 | defer?: DeferTiming;
|
16 | getImport(window: Window): Imported;
|
17 | }
|
18 |
|
19 | export enum Status {
|
20 | Initial = 'Initial',
|
21 | Failed = 'Failed',
|
22 | Complete = 'Complete',
|
23 | Loading = 'Loading',
|
24 | }
|
25 |
|
26 | type Result<Imported = unknown> =
|
27 | | {status: Status.Initial}
|
28 | | {status: Status.Loading}
|
29 | | {status: Status.Failed; error: Error}
|
30 | | {status: Status.Complete; imported: Imported};
|
31 |
|
32 | export 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 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | if (defer === DeferTiming.InViewport) {
|
68 | [intersection, intersectionRef] = useIntersection();
|
69 | }
|
70 |
|
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 | }
|