---
title: "Build Your Own JSON-Render Frontend"
description: "@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/."
canonical_url: "https://devfra.me/guide/build-your-own-json-render-frontend"
---
# 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/.

`@devframes/json-render-ui` is the reference frontend, not the protocol; any
implementation of the renderer contract replaces it. The [Next hub witness](https://github.com/devframes/devframe/tree/main/examples/custom-hub-next) ships a React one at
`src/client/json-render/`.

## The contract

`@devframes/json-render/hub` owns the types:

```ts
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 via `context.rpc.sharedState.get(stateKey)`, render
it as the live spec, re-render on `'updated'`. **Unsubscribe in `dispose`.**
- `{ 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 `basePropSchemas` from
`@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 hub UI provider bundling its own client runtime passes
`createDevframeClientRuntime({ 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:
  ```ts
  import 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 hub UI providers import it lazily (see [renderer
  modules](https://devfra.me/guide/hub-initiate#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`.


## Sitemap

See the full [sitemap](https://devfra.me/sitemap.md) for all pages.
