renderer.tsx 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. "use client";
  2. import { useMemo, useRef, type ReactNode } from "react";
  3. import {
  4. Renderer,
  5. type ComponentRegistry,
  6. type Spec,
  7. DataProvider,
  8. VisibilityProvider,
  9. ActionProvider,
  10. } from "@json-render/react";
  11. import { components, Fallback } from "./catalog/components";
  12. import { executeAction } from "./catalog/actions";
  13. // =============================================================================
  14. // DashboardRenderer
  15. // =============================================================================
  16. type SetData = (
  17. updater: (prev: Record<string, unknown>) => Record<string, unknown>,
  18. ) => void;
  19. interface DashboardRendererProps {
  20. spec: Spec | null;
  21. data?: Record<string, unknown>;
  22. setData?: SetData;
  23. onDataChange?: (path: string, value: unknown) => void;
  24. loading?: boolean;
  25. }
  26. // Build registry - uses refs to avoid recreating on data changes
  27. function buildRegistry(
  28. dataRef: React.RefObject<Record<string, unknown>>,
  29. setDataRef: React.RefObject<SetData | undefined>,
  30. loading?: boolean,
  31. ): ComponentRegistry {
  32. const registry: ComponentRegistry = {};
  33. for (const [name, componentFn] of Object.entries(components)) {
  34. registry[name] = (renderProps: {
  35. element: { props: Record<string, unknown> };
  36. children?: ReactNode;
  37. }) =>
  38. componentFn({
  39. props: renderProps.element.props as never,
  40. children: renderProps.children,
  41. onAction: (a) => {
  42. const setData = setDataRef.current;
  43. const data = dataRef.current;
  44. if (setData) {
  45. executeAction(a.name, a.params, setData, data);
  46. }
  47. },
  48. loading,
  49. });
  50. }
  51. return registry;
  52. }
  53. // Fallback component for unknown types
  54. const fallbackRegistry = (renderProps: { element: { type: string } }) => (
  55. <Fallback type={renderProps.element.type} />
  56. );
  57. export function DashboardRenderer({
  58. spec,
  59. data = {},
  60. setData,
  61. onDataChange,
  62. loading,
  63. }: DashboardRendererProps): ReactNode {
  64. // Use refs to keep registry stable while still accessing latest data/setData
  65. const dataRef = useRef(data);
  66. const setDataRef = useRef(setData);
  67. dataRef.current = data;
  68. setDataRef.current = setData;
  69. // Memoize registry - only changes when loading changes
  70. const registry = useMemo(
  71. () => buildRegistry(dataRef, setDataRef, loading),
  72. [loading],
  73. );
  74. if (!spec) return null;
  75. return (
  76. <DataProvider initialData={data} onDataChange={onDataChange}>
  77. <VisibilityProvider>
  78. <ActionProvider>
  79. <Renderer
  80. spec={spec}
  81. registry={registry}
  82. fallback={fallbackRegistry}
  83. loading={loading}
  84. />
  85. </ActionProvider>
  86. </VisibilityProvider>
  87. </DataProvider>
  88. );
  89. }