Build Your Own JSON-Render Frontend
@devframes/json-render-ui is the reference frontend, not the protocol; any implementation of the renderer contract replaces it. The Next hub witness ships a React one at src/client/json-render/.
The contract
@devframes/json-render/hub owns the types:
import type { JsonRenderDockRenderer } from '@devframes/json-render/hub'
// a hub DockRenderer narrowed to the json-render dock entry
const renderer: JsonRenderDockRenderer = async ({ entry, container, context }) => {
// mount your framework's root into `container`, render `entry.view`
return { dispose() { /* unmount, unsubscribe */ } }
}Resolve the entry's view:
{ stateKey }— subscribe viacontext.rpc.sharedState.get(stateKey), render it as the live spec, re-render on'updated'. Unsubscribe indispose.{ spec }— render the embedded spec directly.
Detect static output via context.rpc.connectionMeta.backend === 'static', disabling action dispatch there.
Behavior expectations
- Actions — a spec action name dispatches the same-named RPC call. Never bridge the reserved built-ins (
setState,pushState,removeState,validateForm) or promise probes (then/catch/finally); surface failures to the view. - Validation — validate element props against
basePropSchemasfrom@devframes/json-render; swap an invalid element for an error placeholder. - Unknown components — a component your registry lacks renders as a placeholder (type + prop-key gist) with a
console.warn; the rest renders. - State reset — reseed spec state only when the view identity changes, not on every update.
Plugging it in
Local registration — a host bundling its client passes
createDevframeClientHost({ renderers: { 'json-render': myRenderer } }); local registrations win over the manifest.A prebuilt renderer module — bundle your renderer as one self-contained browser ES module (framework and styles included), default-exporting the renderer, plus a node helper returning the registration:
tsimport type { DockRendererRegistration } from '@devframes/hub/initiate' export function myRenderer(): DockRendererRegistration { return { type: 'json-render', file: myPrebuiltModulePath } }Compose it with
initHub({ renderers: [myRenderer()] }); the hub serves the module and viewers import it lazily (see renderer modules).
A prebuilt module must be self-styling and shadow-root-safe: deliver your stylesheet into the mount subtree, which may be a shadow root. Read the theme from the container's live dark class, brand color from the inherited --devframe-primary.