import { useEffect, useRef } from 'react' import type { WidgetLoaderProps } from './types' import { useWidgetStore, widgetStoreActions } from '../stores/widget-store' import type { WrapperState } from '../wrapper' /** * Foundation component for all widgets that manages state initialization, synchronization with the widget store, and data transformation pipeline execution. * * @remarks * The WidgetLoader is stateless -- it manages data flow but does not render any UI. Always wrap it with WidgetWrapper for consistent widget appearance. * * @example * ```tsx * * * * * * ``` */ export function WidgetLoader>( props: WidgetLoaderProps, ) { // Subscribe only to this widget's registeredTools via Zustand selector. // The selector returns a stable reference when other widgets change, // avoiding unnecessary re-evaluations of Effect 4. const registeredTools = useWidgetStore( (state) => state.widgets[props.id]?.registeredTools, ) const dataRef = useRef(props.data) const configRef = useRef(props.config) const isMountedRef = useRef(false) useEffect(() => { dataRef.current = props.data configRef.current = props.config }) // Effect 1: Metadata — type, loading, and error states in a single setWidget call. // Merged to reduce store updates from 2 to 1 per widget during initialization. useEffect(() => { widgetStoreActions.setWidget(props.id, { type: props.type, isLoading: props.isLoading ?? false, isFetching: props.isFetching ?? false, error: props.error, }) }, [props.id, props.type, props.isLoading, props.isFetching, props.error]) // Effect 2: Config updates — run through config pipeline useEffect(() => { if (props.config) { void widgetStoreActions.executeConfigPipeline(props.id, props.config) } }, [props.id, props.config]) // Effect 3: Execute tool pipeline when props.data changes useEffect(() => { void widgetStoreActions.executeToolPipeline(props.id, props.data) }, [props.id, props.data]) // Effect 4: Re-execute pipelines when registered tools change. // Uses requestAnimationFrame to coalesce rapid successive registeredTools // changes (e.g., 6 action components each calling registerTool on mount) // into a single pipeline execution instead of 6 pairs. useEffect(() => { if (!isMountedRef.current) { isMountedRef.current = true return } const rafId = requestAnimationFrame(() => { const { executeToolPipeline, executeConfigPipeline } = widgetStoreActions void executeToolPipeline(props.id, dataRef.current) if (configRef.current) { void executeConfigPipeline(props.id, configRef.current) } }) return () => cancelAnimationFrame(rafId) }, [registeredTools, props.id]) return props.children }