@shopify/react-async
Version:
Tools for creating powerful, asynchronously-loaded React components
259 lines (224 loc) • 6.12 kB
JavaScript
import React, { useCallback, useEffect, useRef } from 'react';
import { createResolver, DeferTiming } from '@shopify/async';
import { IntersectionObserver } from '@shopify/react-intersection-observer';
import { useIdleCallback, OnIdle } from '@shopify/react-idle';
import { useHydrationManager, Hydrator } from '@shopify/react-hydrate';
import { useAsync } from './hooks.mjs';
import { AssetTiming } from './types.mjs';
function createAsyncComponent({
id,
load,
defer,
deferHydration,
displayName,
renderLoading = noopRender,
renderError = defaultRenderError,
usePreload: useCustomPreload = noopUse,
usePrefetch: useCustomPrefetch = noopUse,
useKeepFresh: useCustomKeepFresh = noopUse
}) {
const resolver = createResolver({
id,
load
});
const componentName = displayName || displayNameFromId(resolver.id);
const deferred = defer != null;
const progressivelyHydrated = deferHydration != null;
const scriptTiming = deferred || progressivelyHydrated ? AssetTiming.CurrentPage : AssetTiming.Immediate;
const stylesTiming = deferred ? AssetTiming.CurrentPage : AssetTiming.Immediate;
function Async(props) {
const {
resolved: Component,
load,
loading,
error
} = useAsync(resolver, {
scripts: scriptTiming,
styles: stylesTiming,
immediate: !deferred
});
const {
current: startedHydrated
} = useRef(useHydrationManager().hydrated);
if (error) {
return renderError(error);
}
let loadingMarkup = null;
if (progressivelyHydrated && !startedHydrated) {
loadingMarkup = /*#__PURE__*/React.createElement(Loader, {
defer: deferHydration,
load: load,
props: props
});
} else if (loading) {
loadingMarkup = /*#__PURE__*/React.createElement(Loader, {
defer: defer,
load: load,
props: props
});
}
let contentMarkup = null;
const rendered = Component ? /*#__PURE__*/React.createElement(Component, props) : null;
if (progressivelyHydrated && !startedHydrated) {
contentMarkup = rendered ? /*#__PURE__*/React.createElement(Hydrator, {
id: resolver.id
}, rendered) : /*#__PURE__*/React.createElement(Hydrator, {
id: resolver.id
});
} else if (loading) {
contentMarkup = renderLoading(props);
} else {
contentMarkup = rendered;
}
return /*#__PURE__*/React.createElement(React.Fragment, null, contentMarkup, loadingMarkup);
}
Async.displayName = `Async(${componentName})`;
function usePreload(props) {
const {
load
} = useAsync(resolver, {
assets: AssetTiming.NextPage
});
const customPreload = useCustomPreload(props);
return useCallback(() => {
load();
if (customPreload) {
customPreload();
}
}, [load, customPreload]);
}
function usePrefetch(props) {
const {
load
} = useAsync(resolver, {
assets: AssetTiming.NextPage
});
const customPrefetch = useCustomPrefetch(props);
return useCallback(() => {
load();
if (customPrefetch) {
customPrefetch();
}
}, [load, customPrefetch]);
}
function useKeepFresh(props) {
const {
load
} = useAsync(resolver, {
assets: AssetTiming.NextPage
});
const customKeepFresh = useCustomKeepFresh(props);
return useCallback(() => {
load();
if (customKeepFresh) {
customKeepFresh();
}
}, [load, customKeepFresh]);
}
function Preload(options) {
useIdleCallback(usePreload(options));
return null;
}
Preload.displayName = `Async.Preload(${displayName})`;
function Prefetch(options) {
const prefetch = usePrefetch(options);
useEffect(() => {
prefetch();
}, [prefetch]);
return null;
}
Prefetch.displayName = `Async.Prefetch(${displayName})`;
function KeepFresh(options) {
useIdleCallback(useKeepFresh(options));
return null;
}
KeepFresh.displayName = `Async.KeepFresh(${displayName})`;
const FinalComponent = Async;
Reflect.defineProperty(FinalComponent, 'resolver', {
value: resolver,
writable: false
});
Reflect.defineProperty(FinalComponent, 'Preload', {
value: Preload,
writable: false
});
Reflect.defineProperty(FinalComponent, 'Prefetch', {
value: Prefetch,
writable: false
});
Reflect.defineProperty(FinalComponent, 'KeepFresh', {
value: KeepFresh,
writable: false
});
Reflect.defineProperty(FinalComponent, 'usePreload', {
value: usePreload,
writable: false
});
Reflect.defineProperty(FinalComponent, 'usePrefetch', {
value: usePrefetch,
writable: false
});
Reflect.defineProperty(FinalComponent, 'useKeepFresh', {
value: useKeepFresh,
writable: false
});
return FinalComponent;
}
function noopUse() {
return () => {};
}
function noopRender() {
return null;
}
const DEFAULT_DISPLAY_NAME = 'Component';
const FILENAME_REGEX = /([^/]*)\.\w+$/;
function displayNameFromId(id) {
if (!id) {
return DEFAULT_DISPLAY_NAME;
}
const match = FILENAME_REGEX.exec(id);
return match ? match[1] : DEFAULT_DISPLAY_NAME;
}
function defaultRenderError(error) {
if (error) {
throw error;
}
return null;
}
function Loader({
defer,
load,
props
}) {
const handleIntersection = useCallback(({
isIntersecting = true
}) => {
if (isIntersecting) {
load();
}
}, [load]);
useEffect(() => {
if (defer == null || defer === DeferTiming.Mount) {
load();
} else if (typeof defer === 'function' && defer(props)) {
load();
}
}, [defer, load, props]);
if (typeof defer === 'function') {
return null;
}
switch (defer) {
case DeferTiming.Idle:
return /*#__PURE__*/React.createElement(OnIdle, {
perform: load
});
case DeferTiming.InViewport:
return /*#__PURE__*/React.createElement(IntersectionObserver, {
threshold: 0,
onIntersectionChange: handleIntersection
});
default:
return null;
}
}
export { createAsyncComponent };