import { useLayoutEffect, useState, type ReactNode } from 'react' import { WidgetContext, createWidgetStore, hasWidgetStore, getWidgetStore, registerWidgetStore, setWidgetStoreEntry, unregisterWidgetStore, } from '../stores' import type { WidgetStoreApi } from '../stores' export interface ProviderProps { id: string data: unknown isLoading?: boolean isFetching?: boolean error?: unknown formatter?: (value: number) => string labelFormatter?: (value: string | number) => string | number /** * When `true`, the per-widget store survives unmount so transformStates, * search text, fullscreen state, etc. are preserved across remounts. * Consumers using keepAlive must call `deleteWidgetStore(id)` themselves * when the widget is permanently removed — there is no automatic GC. */ keepAlive?: boolean children: ReactNode } /** * Renderer-agnostic widget shell. Creates a per-widget Zustand store * keyed by `id`, syncs the consumer props (`data`, `isLoading`, `error`, * `formatter`, …) onto it, and exposes the `id` via `WidgetContext` so * descendants can resolve their store with `useWidgetId()`. * * The Provider deliberately doesn't know about ECharts — there's no * `optionFactory` prop here. ECharts-backed widgets pass their factory * to ``, which owns the structural * seed + the data-fusion render step entirely. Non-Echart widgets * (Formula, Spread, Range, Table, Category, Markdown) just don't pass * anything ECharts-shaped. */ export function Provider({ id, data, isLoading, isFetching, error, formatter, labelFormatter, keepAlive = false, children, }: ProviderProps) { // Lazy init — reuse an existing keepAlive store, otherwise create one and // synchronously place it in the registry so descendants can call // useWidget(id, ...) on first render. Refcounting + dup-id detection happen // in the mount layout-effect below. const [store] = useState(() => { if (hasWidgetStore(id)) return getWidgetStore(id) const created = createWidgetStore(id, { data, isLoading, isFetching, error, formatter, labelFormatter, }) setWidgetStoreEntry(id, created) return created }) // Registration: refcount the mount for dup-id detection; unregister on // unmount or when the id/store/keepAlive triple changes. useLayoutEffect(() => { registerWidgetStore(id, store) return () => { unregisterWidgetStore(id, { keepAlive }) } }, [id, store, keepAlive]) // Per-prop sync. `useLayoutEffect` so kept-alive remounts apply incoming // props before paint (no flash of stale data when reusing a store). // The pipeline middleware bypasses unchanged fields, so unchanged props // are a no-op even though setState is called every effect run. useLayoutEffect(() => { store.setState({ rawData: data }) }, [store, data]) useLayoutEffect(() => { store.setState({ isLoading: isLoading ?? false, isFetching: isFetching ?? false, error, }) }, [store, isLoading, isFetching, error]) // Sync the consumer's formatter prop to BOTH `rawFormatter` (kept canonical // for actions that need to restore the absolute view) and `formatter` (the // active value). Actions like RelativeData re-apply their override after // `rawFormatter` changes. useLayoutEffect(() => { store.setState({ rawFormatter: formatter, formatter, labelFormatter, }) }, [store, formatter, labelFormatter]) return {children} }