renderer.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { useMemo, useRef, type ReactNode } from "react";
  2. import {
  3. Renderer,
  4. type ComponentRegistry,
  5. type Spec,
  6. StateProvider,
  7. VisibilityProvider,
  8. ActionProvider,
  9. } from "@json-render/react";
  10. import { components, Fallback } from "./catalog/components";
  11. import { executeAction } from "./catalog/actions";
  12. // =============================================================================
  13. // Types
  14. // =============================================================================
  15. type SetState = (
  16. updater: (prev: Record<string, unknown>) => Record<string, unknown>,
  17. ) => void;
  18. export interface StripeRendererProps {
  19. /** The UI spec to render (from json-render) */
  20. spec: Spec | null;
  21. /** Data context for components */
  22. data?: Record<string, unknown>;
  23. /** Function to update data */
  24. setData?: SetState;
  25. /** Callback when data changes */
  26. onStateChange?: (path: string, value: unknown) => void;
  27. /** Whether the spec is currently loading/streaming */
  28. loading?: boolean;
  29. }
  30. // =============================================================================
  31. // Build Registry
  32. // =============================================================================
  33. /**
  34. * Build a component registry from our components map.
  35. * Uses refs to avoid recreating on data changes.
  36. */
  37. function buildRegistry(
  38. dataRef: React.RefObject<Record<string, unknown>>,
  39. setDataRef: React.RefObject<SetState | undefined>,
  40. loading?: boolean,
  41. ): ComponentRegistry {
  42. const registry: ComponentRegistry = {};
  43. for (const [name, Component] of Object.entries(components)) {
  44. const noop = () => {};
  45. registry[name] = (renderProps: {
  46. element: { type: string; props: Record<string, unknown> };
  47. children?: ReactNode;
  48. emit?: (event: string) => void;
  49. }) =>
  50. Component({
  51. element: renderProps.element,
  52. children: renderProps.children,
  53. emit: renderProps.emit ?? noop,
  54. loading,
  55. state: dataRef.current,
  56. getValue: (path: string) => {
  57. const data = dataRef.current;
  58. const parts = path.replace(/^\//, "").split("/");
  59. let current: unknown = data;
  60. for (const part of parts) {
  61. if (current && typeof current === "object" && part in current) {
  62. current = (current as Record<string, unknown>)[part];
  63. } else {
  64. return undefined;
  65. }
  66. }
  67. return current;
  68. },
  69. onAction: (actionName: string, params?: Record<string, unknown>) => {
  70. const setState = setDataRef.current;
  71. if (setState) {
  72. executeAction(actionName, params, setState, dataRef.current);
  73. }
  74. },
  75. });
  76. }
  77. return registry;
  78. }
  79. /**
  80. * Fallback component for unknown types
  81. */
  82. const fallbackRegistry = (renderProps: {
  83. element: { type: string; props: Record<string, unknown> };
  84. }) => <Fallback element={renderProps.element} />;
  85. // =============================================================================
  86. // StripeRenderer Component
  87. // =============================================================================
  88. /**
  89. * Main renderer component for Stripe UIXT.
  90. *
  91. * Wraps the json-render Renderer with all necessary providers
  92. * and connects it to the Stripe component implementations.
  93. */
  94. export function StripeRenderer({
  95. spec,
  96. data = {},
  97. setData,
  98. onStateChange,
  99. loading,
  100. }: StripeRendererProps): ReactNode {
  101. // Use refs to keep registry stable while still accessing latest data/setData
  102. const dataRef = useRef(data);
  103. const setDataRef = useRef(setData);
  104. dataRef.current = data;
  105. setDataRef.current = setData;
  106. // Memoize registry - only changes when loading changes
  107. const registry = useMemo(
  108. () => buildRegistry(dataRef, setDataRef, loading),
  109. [loading],
  110. );
  111. const mergedState = useMemo(
  112. () => (spec?.state ? { ...data, ...spec.state } : data),
  113. [data, spec?.state],
  114. );
  115. if (!spec) return null;
  116. return (
  117. <StateProvider initialState={mergedState} onStateChange={onStateChange}>
  118. <VisibilityProvider>
  119. <ActionProvider>
  120. <Renderer
  121. spec={spec}
  122. registry={registry}
  123. fallback={fallbackRegistry}
  124. loading={loading}
  125. />
  126. </ActionProvider>
  127. </VisibilityProvider>
  128. </StateProvider>
  129. );
  130. }