import { Controller } from '@hotwired/stimulus'; import React, { lazy, Suspense, FunctionComponent } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import invariant from 'tiny-invariant'; type Props = Record; type Loader = () => Promise<{ default: FunctionComponent }>; const componentsRegistry = new Map>(); const components = import.meta.glob('../components/*.tsx'); for (const [path, loader] of Object.entries(components)) { const [filename] = path.split('/').reverse(); const componentClassName = filename.replace(/\.(ts|tsx)$/, ''); console.debug( `Registered lazy default export for "${componentClassName}" component` ); componentsRegistry.set( componentClassName, LoadableComponent(loader as Loader) ); } // Initialize React components when their markup appears into the DOM. // // Example: //
// export class ReactController extends Controller { static values = { component: String, props: Object }; declare readonly componentValue: string; declare readonly propsValue: Props; connect(): void { this.mountComponent(this.element as HTMLElement); } disconnect(): void { unmountComponentAtNode(this.element as HTMLElement); } private mountComponent(node: HTMLElement): void { const componentName = this.componentValue; const props = this.propsValue; const Component = this.getComponent(componentName); invariant( Component, `Cannot find a React component with class "${componentName}"` ); render(, node); } private getComponent(componentName: string): FunctionComponent | null { return componentsRegistry.get(componentName) ?? null; } } const Spinner = () =>
; function LoadableComponent(loader: Loader): FunctionComponent { const LazyComponent = lazy(loader); const Component: FunctionComponent = (props: Props) => ( }> ); return Component; }