Page actions

React Render Spec preview

Play an authored JSON sample and watch trusted components appear as it becomes complete.

Recorded in the real example with an authored local JSON sample. No model requests or narration. Actual-speed footage with reading pauses. Capture details

Read the walkthrough
  1. Choose an authored sample in the specification example.
  2. Play the local JSON stream and inspect the incomplete render.
  3. Finish the stream to inspect the complete component tree. No model or runtime request is involved.
Download the text walkthrough

Describe a small interface as JSON and render it with your own React components. The specification supplies names, props, and child relationships; the registry supplies executable code.

The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.

Keep the registry outside the specification

RenderSpec takes plain specification data and a stable component map. The view receives resolved props and children; a container renders those children where they belong. This complete local example needs no agent. In the trip planner, the same boundary lets a backend describe an itinerary without shipping JavaScript.

import { RenderSpec, type ReactRenderRegistry, type RenderSpecData } from '@threadplane/react/render';
 
const registry: ReactRenderRegistry = {
  Text: ({ props }) => <p>{String(props['text'] ?? '')}</p>,
};
const spec: RenderSpecData = {
  root: 'summary',
  elements: { summary: { type: 'Text', props: { text: 'Weekend in Portland' } } },
};
 
export function TripSummary() {
  return <RenderSpec spec={spec} registry={registry} />;
}

The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.

Play a local sample

Choose Run, pick Heading + Text, Card + Badge or Nested Layout, and press Play. Pause holds the current position. Move Playback position to rewind or advance, Finish displays the complete sample, and Reset clears the current selection. Playing a completed sample starts it again from zero.

The two panels show the rendered view and the literal JSON prefix. These samples run locally and make no agent requests.

specs.ts
import type { PlaybackSample } from './playback';
 
export const localSamples: readonly PlaybackSample[] = Object.freeze(
  [
    {
      label: 'Heading + Text',
      json: JSON.stringify(
        {
          root: 'root',
          elements: {
            root: {
              type: 'Heading',
              props: { content: 'Welcome to React Spec Rendering' },
              children: ['description'],
            },
            description: {
              type: 'Text',
              props: {
                content:
                  'This local JSON sample maps each element to a registered React view.',
              },
            },
          },
        },
        null,
        2
      ),
    },
    {
      label: 'Card + Badge',
      json: JSON.stringify(
        {
          root: 'root',
          elements: {
            root: {
              type: 'Card',
              props: { title: 'Streaming Demo' },
              children: ['badge', 'description'],
            },
            badge: { type: 'Badge', props: { label: 'Live Preview' } },
            description: {
              type: 'Text',
              props: {
                content:
                  'A card receives its badge and text as children from RenderSpec.',
              },
            },
          },
        },
        null,
        2
      ),
    },
    {
      label: 'Nested Layout',
      json: JSON.stringify(
        {
          root: 'root',
          elements: {
            root: {
              type: 'Heading',
              props: { content: 'Multi-Level Nesting' },
              children: ['first', 'second'],
            },
            first: {
              type: 'Card',
              props: { title: 'Section One' },
              children: ['firstText'],
            },
            firstText: {
              type: 'Text',
              props: { content: 'The first card contains its own text view.' },
            },
            second: {
              type: 'Card',
              props: { title: 'Section Two' },
              children: ['secondText'],
            },
            secondText: {
              type: 'Text',
              props: {
                content: 'The second card follows in the authored child order.',
              },
            },
          },
        },
        null,
        2
      ),
    },
  ].map((sample) => Object.freeze(sample))
);

Register four views

The spec names a root and an elements map. Each type selects Heading, Text, Card or Badge from the authored registry. Literal props supply the text. RenderSpec supplies children to each registered view; Heading and Card display those children in the authored order.

views.tsx
import type {
  ReactRenderRegistry,
  RenderViewProps,
} from '@threadplane/react/render';
 
function literal(value: unknown): string {
  return typeof value === 'string' ? value : '';
}
function Heading({ props, children, loading }: RenderViewProps) {
  return (
    <div className="render-heading">
      <h3 aria-busy={loading}>
        {literal(props.content) || (loading ? '…' : '')}
      </h3>
      {children}
    </div>
  );
}
function Text({ props, loading }: RenderViewProps) {
  return (
    <p aria-busy={loading}>{literal(props.content) || (loading ? '…' : '')}</p>
  );
}
function Card({ props, children, loading }: RenderViewProps) {
  return (
    <article className="render-card">
      <h4 aria-busy={loading}>
        {literal(props.title) || (loading ? '…' : '')}
      </h4>
      {children}
    </article>
  );
}
function Badge({ props, loading }: RenderViewProps) {
  return (
    <span className="render-badge" aria-busy={loading}>
      {literal(props.label) || (loading ? '…' : '')}
    </span>
  );
}
export const localRegistry: ReactRenderRegistry = Object.freeze({
  Heading,
  Text,
  Card,
  Badge,
});
app.tsx
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleLayout } from '../../../../shared/react-example';
import { useEffect, useSyncExternalStore } from 'react';
import { RenderSpec } from '@threadplane/react/render';
import type { Playback } from './playback';
import { localRegistry } from './views';
 
export function SpecDemo({
  playback,
  samples,
  onReady,
}: {
  readonly playback: Playback;
  readonly samples: readonly string[];
  readonly onReady: () => void;
}) {
  const state = useSyncExternalStore(
    playback.subscribe,
    playback.getSnapshot,
    playback.getSnapshot
  );
  useEffect(onReady, [onReady]);
  const phase =
    state.phase === 'error'
      ? 'Unavailable'
      : state.phase[0].toUpperCase() + state.phase.slice(1);
  return (
    <ExampleLayout kind="render" className="spec-demo">
      <header>
        <h1>Render Spec</h1>
      </header>
      <p>Watch a local JSON sample become a React view as its text arrives.</p>
      <section aria-label="Playback controls" className="controls">
        <label>
          Sample
          <select
            value={state.selected}
            onChange={(event) => playback.select(Number(event.target.value))}
          >
            {samples.map((label, index) => (
              <option key={index} value={index}>
                {label}
              </option>
            ))}
          </select>
        </label>
        <div className="buttons">
          <button
            type="button"
            disabled={state.phase === 'error'}
            onClick={state.playing ? playback.pause : playback.play}
          >
            {state.playing ? 'Pause' : 'Play'}
          </button>
          <button
            type="button"
            disabled={state.phase === 'error'}
            onClick={playback.finish}
          >
            Finish
          </button>
          <button type="button" onClick={playback.reset}>
            Reset
          </button>
        </div>
        <label>
          Playback position
          <input
            type="range"
            min={0}
            max={state.total}
            step={1}
            value={state.position}
            disabled={state.phase === 'error'}
            onChange={(event) => playback.seek(Number(event.target.value))}
          />
        </label>
        <p role="status">
          {phase} · {state.position} characters
        </p>
        {state.phase === 'error' && (
          <p role="alert">
            This sample could not be displayed. Choose another sample to
            continue.
          </p>
        )}
      </section>
      <div className="panels">
        <section aria-label="Render output">
          <h2>Render output</h2>
          {state.spec ? (
            <RenderSpec
              spec={state.spec}
              registry={localRegistry}
              loading={state.position < state.total}
            />
          ) : (
            <p className="empty">Play a sample to see its view.</p>
          )}
        </section>
        <section aria-label="Streaming JSON">
          <h2>Streaming JSON</h2>
          <pre>{state.rawJson}</pre>
        </section>
      </div>
    </ExampleLayout>
  );
}

Keep partial data owned

The caller validates each bounded complete sample before playback, then copies and freezes the known partial display tree. Incomplete view names and missing child references wait until they are ready. These authored samples permit four view types and literal string props; they do not use actions, expressions or state bindings.

projection.ts
import type {
  RenderElementData,
  RenderSpecData,
} from '@threadplane/react/render';
 
const TYPES = ['Heading', 'Text', 'Card', 'Badge'];
const ID = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
const MAX_NODES = 32;
const MAX_TEXT = 16 * 1024;
 
function invalid(): never {
  throw new TypeError('Invalid local render data.');
}
 
function record(input: unknown): Record<string, unknown> {
  if (
    input === null ||
    typeof input !== 'object' ||
    Array.isArray(input) ||
    ![null, Object.prototype].includes(Object.getPrototypeOf(input)) ||
    Object.getOwnPropertySymbols(input).length
  )
    invalid();
  const entries = Object.entries(Object.getOwnPropertyDescriptors(input));
  if (entries.length > MAX_NODES) invalid();
  const result = Object.create(null) as Record<string, unknown>;
  for (const [key, descriptor] of entries) {
    if (!descriptor.enumerable || !('value' in descriptor)) invalid();
    Object.defineProperty(result, key, {
      value: descriptor.value,
      enumerable: true,
    });
  }
  return result;
}
 
function fields(
  input: Record<string, unknown>,
  allowed: readonly string[]
): void {
  if (Object.keys(input).some((key) => !allowed.includes(key))) invalid();
}
 
function references(input: unknown, complete: boolean): readonly string[] {
  if (input === undefined && !complete) return [];
  if (
    !Array.isArray(input) ||
    Object.getPrototypeOf(input) !== Array.prototype ||
    input.length > MAX_NODES ||
    Object.getOwnPropertySymbols(input).length
  )
    invalid();
  const descriptors = Object.getOwnPropertyDescriptors(input);
  for (const [key, descriptor] of Object.entries(descriptors))
    if (
      !('value' in descriptor) ||
      (key !== 'length' && !/^(0|[1-9][0-9]*)$/.test(key))
    )
      invalid();
  const result: string[] = [];
  for (let index = 0; index < input.length; index++) {
    const value = descriptors[String(index)]?.value;
    if ((value === undefined || value === '') && !complete) continue;
    if (typeof value !== 'string' || !ID.test(value) || result.includes(value))
      invalid();
    result.push(value);
  }
  return result;
}
 
function project(input: unknown, complete: boolean): RenderSpecData | null {
  if (input === null && !complete) return null;
  const source = record(input);
  fields(source, ['root', 'elements']);
  if (
    source.root === undefined ||
    source.root === '' ||
    source.elements === undefined
  ) {
    if (complete) invalid();
    return null;
  }
  if (typeof source.root !== 'string' || !ID.test(source.root)) invalid();
  const sourceElements = record(source.elements);
  const elements = Object.create(null) as Record<string, RenderElementData>;
  const childLists = new Map<string, readonly string[]>();
  let textSize = 0;
  for (const [key, inputElement] of Object.entries(sourceElements)) {
    if (!ID.test(key)) invalid();
    if (inputElement === undefined && !complete) continue;
    const element = record(inputElement);
    fields(element, ['type', 'props', 'children']);
    if (element.type === undefined && !complete) continue;
    if (typeof element.type !== 'string') invalid();
    if (!TYPES.includes(element.type)) {
      if (complete) invalid();
      continue;
    }
    const rawProps = element.props === undefined ? {} : record(element.props);
    fields(rawProps, [
      element.type === 'Card'
        ? 'title'
        : element.type === 'Badge'
        ? 'label'
        : 'content',
    ]);
    const props = Object.create(null) as Record<string, string>;
    for (const [prop, value] of Object.entries(rawProps)) {
      if (value === undefined && !complete) continue;
      if (typeof value !== 'string' || (textSize += value.length) > MAX_TEXT)
        invalid();
      Object.defineProperty(props, prop, { value, enumerable: true });
    }
    const children = Object.hasOwn(element, 'children')
      ? references(element.children, complete)
      : undefined;
    if (children) childLists.set(key, children);
    Object.defineProperty(elements, key, {
      value: { type: element.type, props: Object.freeze(props) },
      enumerable: true,
    });
  }
  for (const [key, children] of childLists) {
    if (complete && children.some((child) => !Object.hasOwn(elements, child)))
      invalid();
    Object.defineProperty(elements[key], 'children', {
      value: Object.freeze(
        children.filter((child) => Object.hasOwn(elements, child))
      ),
      enumerable: true,
    });
  }
  const heights = new Map<string, number>();
  const active = new Set<string>();
  function height(key: string): number {
    const known = heights.get(key);
    if (known !== undefined) return known;
    if (active.has(key)) invalid();
    active.add(key);
    let result = 1;
    for (const child of elements[key].children ?? [])
      result = Math.max(result, 1 + height(child));
    active.delete(key);
    if (result > 16) invalid();
    heights.set(key, result);
    return result;
  }
  for (const key of Object.keys(elements)) {
    height(key);
    Object.freeze(elements[key]);
  }
  if (!Object.hasOwn(elements, source.root)) {
    if (complete) invalid();
    return null;
  }
  return Object.freeze({
    root: source.root,
    elements: Object.freeze(elements),
  });
}
 
/** Copy the known display subset; the incremental parser retains its own mutable tree. */
export function projectPartialSpec(input: unknown): RenderSpecData | null {
  return project(input, false);
}
 
/** Admit a complete authored local sample before any playback begins. */
export function validateCompleteSpec(input: unknown): RenderSpecData {
  return project(input, true) ?? invalid();
}

A private parser owns the stream. Seeking rebuilds it from the selected prefix. Pause, sample changes, Reset and page exit cancel the current animation generation, so an old callback cannot replace the current display. Complete means the parser finished and its copied output matches the validated source.

playback.ts
import type { RenderSpecData } from '@threadplane/react/render';
import { createPartialJsonParser, materialize } from '@cacheplane/partial-json';
import { projectPartialSpec, validateCompleteSpec } from './projection';
 
export interface PlaybackSample {
  readonly label: string;
  readonly json: string;
}
export interface FrameScheduler {
  request(callback: FrameRequestCallback): number;
  cancel(id: number): void;
}
export interface PlaybackSnapshot {
  readonly selected: number;
  readonly label: string;
  readonly position: number;
  readonly total: number;
  readonly phase: 'paused' | 'playing' | 'complete' | 'error';
  readonly playing: boolean;
  readonly rawJson: string;
  readonly spec: RenderSpecData | null;
}
export interface Playback {
  getSnapshot(): PlaybackSnapshot;
  subscribe(listener: () => void): () => void;
  play(): void;
  pause(): void;
  seek(position: number): void;
  select(index: number): void;
  finish(): void;
  reset(): void;
  dispose(): void;
}
 
export function createPlayback(
  samples: readonly PlaybackSample[],
  scheduler: FrameScheduler = {
    request: (callback) => requestAnimationFrame(callback),
    cancel: (id) => cancelAnimationFrame(id),
  }
): Playback {
  const owned = samples.map((sample) => ({
    label: sample.label,
    json: sample.json,
  }));
  const listeners = new Set<() => void>();
  let live = true;
  let generation = 0;
  let frame: number | undefined;
  let parser = createPartialJsonParser();
  let admitted: RenderSpecData | null = null;
  let snapshot: PlaybackSnapshot;
 
  function revoke(): number {
    generation++;
    if (frame !== undefined) scheduler.cancel(frame);
    frame = undefined;
    return generation;
  }
 
  function publish(next: PlaybackSnapshot): void {
    snapshot = Object.freeze(next);
    const current = generation;
    for (const listener of [...listeners]) {
      if (!live || current !== generation) break;
      if (!listeners.has(listener)) continue;
      try {
        listener();
      } catch {
        /* A subscriber cannot take ownership of playback. */
      }
    }
  }
 
  function initialize(selected: number, notify: boolean): void {
    parser = createPartialJsonParser();
    admitted = null;
    const sample = owned[selected];
    try {
      if (!sample || sample.json.length > 16 * 1024)
        throw Error('Invalid sample');
      // Incremental parsers can retain malformed input as streaming forever.
      admitted = validateCompleteSpec(JSON.parse(sample.json));
    } catch {
      /* Only authored, bounded complete JSON may be played. */
    }
    const next: PlaybackSnapshot = {
      selected,
      label: sample?.label ?? 'Unavailable sample',
      position: 0,
      total: sample?.json.length ?? 0,
      phase: admitted ? 'paused' : 'error',
      playing: false,
      rawJson: '',
      spec: null,
    };
    if (notify) publish(next);
    else snapshot = Object.freeze(next);
  }
 
  function advance(position: number, playing: boolean): void {
    const source = owned[snapshot.selected].json;
    try {
      parser.push(source.slice(snapshot.position, position));
      const complete = position === source.length;
      if (complete) parser.finish();
      const value = parser.root ? materialize(parser.root) : null;
      const spec = complete
        ? validateCompleteSpec(value)
        : projectPartialSpec(value);
      if (
        complete &&
        (parser.root?.status !== 'complete' ||
          JSON.stringify(spec) !== JSON.stringify(admitted))
      )
        throw Error('Incomplete sample');
      publish({
        ...snapshot,
        position,
        rawJson: source.slice(0, position),
        spec,
        playing: playing && !complete,
        phase: complete ? 'complete' : playing ? 'playing' : 'paused',
      });
    } catch {
      revoke();
      publish({
        ...snapshot,
        phase: 'error',
        playing: false,
        rawJson: '',
        spec: null,
      });
    }
  }
 
  function schedule(current: number): void {
    if (
      !live ||
      generation !== current ||
      !snapshot.playing ||
      frame !== undefined
    )
      return;
    frame = scheduler.request(() => {
      if (!live || generation !== current || !snapshot.playing) return;
      frame = undefined;
      advance(Math.min(snapshot.total, snapshot.position + 4), true);
      schedule(current);
    });
  }
 
  function seek(position: number): void {
    if (!live || !Number.isFinite(position) || !admitted) return;
    revoke();
    parser = createPartialJsonParser();
    const target = Math.max(0, Math.min(snapshot.total, Math.floor(position)));
    // Rebuild privately, then publish once so a subscriber cannot interrupt a reset.
    snapshot = Object.freeze({
      ...snapshot,
      position: 0,
      rawJson: '',
      spec: null,
      playing: false,
      phase: 'paused',
    });
    advance(target, false);
  }
 
  initialize(0, false);
  return {
    getSnapshot: () => snapshot,
    subscribe(listener) {
      if (live) listeners.add(listener);
      return () => {
        listeners.delete(listener);
      };
    },
    play() {
      if (!live || !admitted || snapshot.playing) return;
      const current = revoke();
      if (snapshot.phase === 'complete') {
        parser = createPartialJsonParser();
        snapshot = Object.freeze({
          ...snapshot,
          position: 0,
          rawJson: '',
          spec: null,
        });
      }
      publish({ ...snapshot, playing: true, phase: 'playing' });
      schedule(current);
    },
    pause() {
      if (!live || !snapshot.playing) return;
      revoke();
      publish({ ...snapshot, playing: false, phase: 'paused' });
    },
    seek,
    select(index) {
      if (
        !live ||
        !Number.isInteger(index) ||
        index < 0 ||
        index >= owned.length
      )
        return;
      revoke();
      initialize(index, true);
    },
    finish() {
      seek(snapshot.total);
    },
    reset() {
      seek(0);
    },
    dispose() {
      if (!live) return;
      live = false;
      revoke();
      listeners.clear();
      snapshot = Object.freeze({
        ...snapshot,
        playing: false,
        phase: snapshot.phase === 'playing' ? 'paused' : snapshot.phase,
      });
    },
  };
}

Source build and verification

From a Threadplane checkout with dependencies installed:

npx nx build cockpit-render-spec-rendering-react
npx nx e2e cockpit-render-spec-rendering-react

The isolated build installs local React, core and content package artifacts plus root-locked presentation dependencies. It verifies native RenderSpec and parser modules without a backend runtime. Browser coverage exercises all samples, partial playback, rewind, cancellation and narrow screens. Select Angular to read and run the existing Angular implementation.

Looking for something specific?