import { Component, Suspense, useEffect, type ReactNode, type ComponentType, } from 'react'; import {AsyncModule, type AsyncModuleLoader} from '@quilted/async'; import type {AssetLoadTiming} from '@quilted/react-browser/server'; import {useHydrated} from './hooks/hydration.ts'; import {useAsyncModuleAssets} from './hooks/module.ts'; export interface AsyncComponentProps { module: AsyncModule<{default: ComponentType}>; props?: Props; server?: boolean; client?: boolean | 'render' | 'defer'; preload?: boolean; renderLoading?: ReactNode | ((props: Props) => ReactNode); } export class AsyncComponent extends Component< AsyncComponentProps > { static from( moduleOrImport: | AsyncModule<{default: ComponentType}> | AsyncModuleLoader<{default: ComponentType}>, { name, ...options }: Pick< AsyncComponentProps, 'server' | 'client' | 'preload' | 'renderLoading' > & {name?: string} = {}, ): ComponentType & { readonly module: AsyncModule<{default: ComponentType}>; readonly Preload: ComponentType<{}>; load(): Promise>; } { const module = moduleOrImport instanceof AsyncModule ? moduleOrImport : new AsyncModule(moduleOrImport); function AsyncComponentInternal(props: Props) { return ; } Object.assign(AsyncComponentInternal, { module, displayName: `Async(${name ?? displayNameFromId(module.id)})`, load: () => module.load().then((resolved) => resolved.default), Preload: function Preload() { useAsyncModuleAssets(module, {scripts: 'preload', styles: 'preload'}); useEffect(() => { module.load().catch(() => {}); }); }, }); return AsyncComponentInternal as any; } private Component = function Component({ module, props, client, renderLoading, }: AsyncComponentProps) { // If we aren’t hydrating and we don’t have a component yet, we need to suspend, // so any server-rendered content can be preserved until hydration is performed. if ( typeof document === 'object' && (client === false || client === 'defer') && module.status === 'pending' ) { throw module.promise; } const Component = module.module?.default; if (Component == null) { throw module.load(); } return renderLoading ? ( ) : ( ); }; render() { const { module, props, server = true, client = true, preload = client !== false, 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(); if (!server) { if (!isBrowser) { return normalizeRender(renderLoading, props); } if (!hydrated) { return normalizeRender(renderLoading, props); } } if (client === false && isBrowser) { return null; } return renderLoading ? ( ) : ( ); } } function normalizeRender( render?: ReactNode | ((props: Props) => ReactNode), props: Props = {} as any, ) { return typeof render === 'function' ? render(props) : render ?? null; } function displayNameFromId(id?: string) { if (!id) { return 'Component'; } return id.split('_')[0]; }