Skip to content

Renderer implementation

DynUI renders a validated UITree. The renderer should be deliberately boring: component ids map to known app components, slot children resolve recursively, and unknown or failing components degrade safely.

The model never emits markup or code. The app renderer is the only thing that turns a tree into UI.

Create a registry from manifest component ids to renderer functions:

type Renderer = (
props: Record<string, unknown>,
children: React.ReactNode
) => React.ReactNode;
const registry: Record<string, Renderer> = {
"recovery-score-card": RecoveryScoreCard,
"readiness-panel": ReadinessPanel,
};

Keep this registry close to the app components it renders. It is app code, not a generic DynUI package.

Keep a RendererSpec beside the registry so CI can prove a manifest is renderable before the app receives a tree.

The spec should describe:

  • supported component ids;
  • supported variants;
  • required and optional data bindings the renderer consumes;
  • supported slots;
  • compatible component version ranges;
  • design tokens the renderer can map to native styles;
  • minimum renderer version, if applicable.

Run the compatibility check whenever a manifest changes. A valid manifest is not enough; the renderer must also prove it can render that manifest.

Only render trees that came from generateScreen with validation.ok === true, or trees you explicitly validate before render:

import { validateRenderableTree } from "@dynui/validate";
const result = validateRenderableTree(tree, manifest, {
surface,
profile,
data,
experiments,
});
if (!result.ok) {
showFallback();
return;
}
render(result.tree);

validateTreeStructure is structural only. It is not sufficient for rendering because it cannot see consent, data, experiments, or the current surface.

If a component declares slots, resolve children recursively and pass them into the parent component’s own layout. Do not flatten the tree unless the manifest explicitly models a flat layout.

This distinction matters. Slot composition is where a model can improve on a deterministic ranking engine: it can group known components into known containers without inventing new UI.

Renderer behavior should be deterministic for a given tree and data payload.

Define visible, boring failure behavior:

Failure Recommended behavior
Unknown component Show a safe fallback or suppress with telemetry, depending on surface risk
Invalid tree Do not render; show the app-owned fallback state
Invalid slot child Show the parent without the invalid child or show a safe fallback
Component throw Isolate with a per-component error boundary
Missing optional data Render the component’s designed empty state
Missing required data Suppress the component unless backed by safe fallbackData
Explicit unrenderable Show an app-owned empty or error state

Record render errors separately from exposures. A component should count as an exposure only after it actually rendered.

At minimum, CI should run:

  • manifest schema validation;
  • manifest lint;
  • manifest diff against the last accepted manifest;
  • renderer compatibility;
  • unit tests for slot resolution;
  • no-consent and neutral-output tests;
  • visual or browser checks for representative valid and invalid fixtures.

The reference React Native app implements these ideas in apps/fitness-app/src/renderer/: registry mapping, registry compatibility, nested-slot resolution, and per-component error boundaries.

Have a manifest rollback path. The fastest safe rollback is usually restoring the previous accepted manifest or disabling the DynUI-powered surface behind your existing release mechanism.

Avoid deploying a manifest before the renderer that supports it.