Files
Chris Tate 3201854481 external store adapter for state management (#139)
* external store adapter for state management

Introduces a `StateStore` interface that lets users plug in their own state management (Redux, Zustand, XState, etc.) instead of being locked into the internal `useState`-based store.

- Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`
- `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode
- When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)
- When `store` is omitted, everything works exactly as before (fully backward compatible)
- Applied across all platform packages: react, react-native, react-pdf

* improvements

* update docs

* improvements

* fixes

* fix CI

* add store adapters

* fixes

* fixes

* fixes

* fixes

* e2e tests

* improvements

* fixes

* fixes

* fixes

* fixes

* update lockfile for widened react peer deps

* fix dashboard build
2026-02-23 23:12:57 -06:00

77 lines
1.9 KiB
TypeScript

"use client";
import { useMemo, useRef, type ReactNode } from "react";
import {
Renderer,
type ComponentRenderer,
type Spec,
StateProvider,
VisibilityProvider,
ActionProvider,
} from "@json-render/react";
import { registry, Fallback, handlers as createHandlers } from "./registry";
// =============================================================================
// DashboardRenderer
// =============================================================================
type SetState = (
updater: (prev: Record<string, unknown>) => Record<string, unknown>,
) => void;
interface DashboardRendererProps {
spec: Spec | null;
state?: Record<string, unknown>;
setState?: SetState;
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
loading?: boolean;
}
// Fallback component for unknown types
const fallback: ComponentRenderer = ({ element }) => (
<Fallback type={element.type} />
);
export function DashboardRenderer({
spec,
state = {},
setState,
onStateChange,
loading,
}: DashboardRendererProps): ReactNode {
// Use refs so action handlers always see the latest state/setState
const stateRef = useRef(state);
const setStateRef = useRef(setState);
stateRef.current = state;
setStateRef.current = setState;
// Create ActionProvider-compatible handlers using getters so they
// always read the latest state/setState from refs
const actionHandlers = useMemo(
() =>
createHandlers(
() => setStateRef.current,
() => stateRef.current,
),
[],
);
if (!spec) return null;
return (
<StateProvider initialState={state} onStateChange={onStateChange}>
<VisibilityProvider>
<ActionProvider handlers={actionHandlers}>
<Renderer
spec={spec}
registry={registry}
fallback={fallback}
loading={loading}
/>
</ActionProvider>
</VisibilityProvider>
</StateProvider>
);
}