UNPKG

@shopify/react-async

Version:

Tools for creating powerful, asynchronously-loaded React components

259 lines (224 loc) 6.12 kB
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 };