diff --git a/.changeset/slow-singers-explode.md b/.changeset/slow-singers-explode.md new file mode 100644 index 000000000..06c9e7498 --- /dev/null +++ b/.changeset/slow-singers-explode.md @@ -0,0 +1,5 @@ +--- +'@quilted/preact-async': patch +--- + +Allow customized wrapping of `AsyncComponent` content diff --git a/packages/preact-async/source/AsyncComponent.tsx b/packages/preact-async/source/AsyncComponent.tsx index d0ab884fd..4774a92c5 100644 --- a/packages/preact-async/source/AsyncComponent.tsx +++ b/packages/preact-async/source/AsyncComponent.tsx @@ -14,9 +14,19 @@ export interface AsyncComponentProps { server?: boolean; client?: boolean | 'render' | 'defer'; preload?: boolean; + render?: ( + element: ComponentChildren, + props: AsyncComponentProps, + ) => ComponentChildren; renderLoading?: ComponentChildren | ((props: Props) => ComponentChildren); } +export type AsyncComponentType = ComponentType & { + readonly module: AsyncModule<{default: ComponentType}>; + readonly Preload: ComponentType<{}>; + load(): Promise>; +}; + export class AsyncComponent extends Component< AsyncComponentProps > { @@ -29,13 +39,9 @@ export class AsyncComponent extends Component< ...options }: Pick< AsyncComponentProps, - 'server' | 'client' | 'preload' | 'renderLoading' + 'server' | 'client' | 'preload' | 'render' | 'renderLoading' > & {name?: string} = {}, - ): ComponentType & { - readonly module: AsyncModule<{default: ComponentType}>; - readonly Preload: ComponentType<{}>; - load(): Promise>; - } { + ): AsyncComponentType { const module = moduleOrImport instanceof AsyncModule ? moduleOrImport @@ -60,6 +66,58 @@ export class AsyncComponent extends Component< return AsyncComponentInternal as any; } + static useAssets(props: AsyncComponentProps) { + if (typeof document === 'object') return; + + const { + module, + server = true, + client = true, + preload = client !== false, + } = props; + + const hydrate = + client === true || client === 'render' ? 'immediate' : 'defer'; + + let scriptTiming: AssetLoadTiming; + let styleTiming: AssetLoadTiming; + + if (server) { + // If we are server rendering, we always have to load the styles for an + // async component synchronously. + styleTiming = 'load'; + + if (hydrate === 'immediate') { + // If we are going to hydrate immediately, we need the assets immediately, + // too. + scriptTiming = 'load'; + } else if (preload) { + // If we are going to hydrate later, and the consumer wants to preload, + // we will preload the scripts for later. + scriptTiming = 'preload'; + } else { + // We don’t need the scripts right away, and the consumer doesn’t want + // to preload, so we just won’t load the scripts at all — the client can + // do that if it wants later on! + scriptTiming = 'never'; + } + } else if (preload) { + // We aren’t server rendering, but the consumer wants to preload the assets + // for the component. + styleTiming = 'preload'; + scriptTiming = 'preload'; + } else { + // Not server rendering, and not preloading... We’ll leave it up to the client! + styleTiming = 'never'; + scriptTiming = 'never'; + } + + useAsyncModuleAssets(module, { + scripts: scriptTiming, + styles: styleTiming, + }); + } + private Component = function Component({ module, props, @@ -97,86 +155,46 @@ export class AsyncComponent extends Component< props, server = true, client = true, - preload = client !== false, + render = defaultRender, renderLoading, } = this.props; - const {Component} = this; - - const isBrowser = typeof document === 'object'; - - const hydrate = - client === true || client === 'render' ? 'immediate' : 'defer'; - - if (typeof document !== 'object') { - let scriptTiming: AssetLoadTiming; - let styleTiming: AssetLoadTiming; - - if (server) { - // If we are server rendering, we always have to load the styles for an - // async component synchronously. - styleTiming = 'load'; - - if (hydrate === 'immediate') { - // If we are going to hydrate immediately, we need the assets immediately, - // too. - scriptTiming = 'load'; - } else if (preload) { - // If we are going to hydrate later, and the consumer wants to preload, - // we will preload the scripts for later. - scriptTiming = 'preload'; - } else { - // We don’t need the scripts right away, and the consumer doesn’t want - // to preload, so we just won’t load the scripts at all — the client can - // do that if it wants later on! - scriptTiming = 'never'; - } - } else if (preload) { - // We aren’t server rendering, but the consumer wants to preload the assets - // for the component. - styleTiming = 'preload'; - scriptTiming = 'preload'; - } else { - // Not server rendering, and not preloading... We’ll leave it up to the client! - styleTiming = 'never'; - scriptTiming = 'never'; - } - - useAsyncModuleAssets(module, { - scripts: scriptTiming, - styles: styleTiming, - }); - } if (module.error) { throw module.error; } - const hydrated = useHydrated(); + const {Component} = this; - if (!server) { - if (!isBrowser) { - return normalizeRender(renderLoading, props); - } + const isBrowser = typeof document === 'object'; - if (!hydrated) { - return normalizeRender(renderLoading, props); - } - } + const hydrated = useHydrated(); - if (client === false && isBrowser) { - return null; - } + let content: ComponentChildren = null; - return hasLoadingContent(renderLoading) ? ( - + if (!server && (!isBrowser || !hydrated)) { + content = normalizeRender(renderLoading, props); + } else if (client !== false || !isBrowser) { + content = hasLoadingContent(renderLoading) ? ( + + + + ) : ( - - ) : ( - - ); + ); + } + + return render(content, this.props); } } +function defaultRender( + content: ComponentChildren, + props: AsyncComponentProps, +) { + if (typeof document !== 'object') AsyncComponent.useAssets(props); + return content; +} + function hasLoadingContent( content?: ComponentChildren | ((...args: any) => ComponentChildren), ) { @@ -187,7 +205,7 @@ function hasLoadingContent( function normalizeRender( render?: ComponentChildren | ((props: Props) => ComponentChildren), props: Props = {} as any, -) { +): ComponentChildren { return typeof render === 'function' ? render(props) : render ?? null; } diff --git a/packages/preact-async/source/index.ts b/packages/preact-async/source/index.ts index c96996d2e..ef232a3bd 100644 --- a/packages/preact-async/source/index.ts +++ b/packages/preact-async/source/index.ts @@ -1,6 +1,10 @@ export * from '@quilted/async'; -export {AsyncComponent} from './AsyncComponent.tsx'; +export { + AsyncComponent, + type AsyncComponentProps, + type AsyncComponentType, +} from './AsyncComponent.tsx'; export {AsyncContext} from './AsyncContext.tsx'; export {useAsyncActionCache} from './context.ts';