Page actions

React Computed Functions preview

Transform values in a generated layout using a small set of functions your application provides. A specification can name a calculation without carrying 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.

Supply pure synchronous functions

The functions prop maps names to RenderComputedFunction implementations. They receive owned, frozen arguments and return plain render values. Keep them pure: React may evaluate them again during rendering. Network requests and state writes belong in explicit application actions, not a computed expression.

import type { ReactRenderFunctions } from '@threadplane/react/render';
 
export const tripFunctions: ReactRenderFunctions = {
  uppercase: ({ text }) => typeof text === 'string' ? text.toUpperCase() : '',
};

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

Play a local sample

Choose Run, select Text Transforms, Data Display or Mixed Functions, then press Play. Pause holds the current position. Move Playback position to rewind or advance, Finish displays the complete sample, and Reset clears playback for the selected sample. Playing a completed sample starts it again from zero. Changing samples starts fresh and paused.

Text Transforms produces HELLO WORLD and gnimaerts. Data Display multiplies 7 by 6 to produce 42 and formats its date. Mixed Functions produces 60, COMPUTED FUNCTIONS and a second date. Date formatting follows the viewer’s browser locale and time zone.

specs.ts
import type { PlaybackSample } from './playback';
 
export const localSamples: readonly PlaybackSample[] = [
  {
    label: 'Text Transforms',
    json: JSON.stringify(
      {
        root: 'root',
        elements: {
          root: {
            type: 'Heading',
            props: { content: 'Text Transforms' },
            children: ['upper', 'reversed'],
          },
          upper: {
            type: 'Value',
            props: {
              label: 'Uppercase',
              value: { $computed: 'uppercase', args: { value: 'hello world' } },
            },
          },
          reversed: {
            type: 'Value',
            props: {
              label: 'Reversed',
              value: { $computed: 'reverse', args: { value: 'streaming' } },
            },
          },
        },
      },
      null,
      2
    ),
  },
  {
    label: 'Data Display',
    json: JSON.stringify(
      {
        root: 'root',
        elements: {
          root: {
            type: 'Card',
            props: { title: 'Formatted Data' },
            children: ['date', 'product'],
          },
          date: {
            type: 'Value',
            props: {
              label: 'Formatted Date',
              value: {
                $computed: 'formatDate',
                args: { value: '2024-06-15T12:00:00Z' },
              },
            },
          },
          product: {
            type: 'Value',
            props: {
              label: 'Multiply 7 x 6',
              value: { $computed: 'multiply', args: { a: 7, b: 6 } },
            },
          },
        },
      },
      null,
      2
    ),
  },
  {
    label: 'Mixed Functions',
    json: JSON.stringify(
      {
        root: 'root',
        elements: {
          root: {
            type: 'Heading',
            props: { content: 'Mixed Functions' },
            children: ['calc', 'transform', 'format'],
          },
          calc: {
            type: 'Value',
            props: {
              label: 'Multiply 12 x 5',
              value: { $computed: 'multiply', args: { a: 12, b: 5 } },
            },
          },
          transform: {
            type: 'Value',
            props: {
              label: 'Uppercase',
              value: {
                $computed: 'uppercase',
                args: { value: 'computed functions' },
              },
            },
          },
          format: {
            type: 'Value',
            props: {
              label: 'Date',
              value: {
                $computed: 'formatDate',
                args: { value: '2025-01-01T00:00:00Z' },
              },
            },
          },
        },
      },
      null,
      2
    ),
  },
];

Supply pure functions

The caller passes an owned function map through RenderSpec.functions. A JSON value names a function with $computed and supplies its args. The native resolver resolves those arguments, invokes the registered function and supplies its returned value to the registered view. These samples preserve those expressions as data throughout playback.

Functions must be pure and synchronous. The renderer may evaluate them repeatedly. It supplies recursively owned, frozen arguments and accepts plain render-data results; async work and side effects belong outside these callbacks.

functions.ts
import type { ReactRenderFunctions } from '@threadplane/react/render';
 
/** Pure synchronous calculations; locale formatting follows the viewer's browser. */
export const localFunctions: ReactRenderFunctions = Object.freeze({
  uppercase: ({ value }) =>
    typeof value === 'string' ? value.toUpperCase() : undefined,
  reverse: ({ value }) =>
    typeof value === 'string' ? value.split('').reverse().join('') : undefined,
  multiply: ({ a, b }) =>
    typeof a === 'number' && typeof b === 'number' ? a * b : undefined,
  formatDate: ({ value }) => {
    if (typeof value !== 'string') return undefined;
    const date = new Date(value);
    return Number.isFinite(date.getTime())
      ? date.toLocaleDateString()
      : undefined;
  },
});
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';
import { localFunctions } from './functions';
 
export function ComputedDemo({
  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>Computed Functions</h1>
      </header>
      <p>
        Watch three local samples transform text, multiply numbers and format
        dates. Calculations wait for complete arguments. Dates follow your
        browser’s locale.
      </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}
              functions={localFunctions}
              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>
  );
}

Heading, Card and Value display the resolved props and authored child order.

views.tsx
import type {
  ReactRenderRegistry,
  RenderReadinessEntry,
  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 Value({ props, loading }: RenderViewProps) {
  const value =
    typeof props.value === 'string' ||
    (typeof props.value === 'number' && Number.isFinite(props.value))
      ? String(props.value)
      : '';
  return (
    <dl className="render-value" aria-busy={loading}>
      <dt>{literal(props.label)}</dt>
      <dd>{value || (loading ? '…' : '')}</dd>
    </dl>
  );
}
function Card({ props, children, loading }: RenderViewProps) {
  return (
    <article className="render-card">
      <h4 aria-busy={loading}>
        {literal(props.title) || (loading ? '…' : '')}
      </h4>
      {children}
    </article>
  );
}
const valueReady: RenderReadinessEntry['isReady'] = (props) =>
  typeof props.value === 'string' ||
  (typeof props.value === 'number' && Number.isFinite(props.value));
 
function PendingValue({ props }: RenderViewProps) {
  const label = literal(props.label);
  return (
    <dl
      className="render-value"
      role="group"
      aria-label={`${label} pending`}
      aria-busy={true}
    >
      <dt>{label}</dt>
      <dd>…</dd>
    </dl>
  );
}
export const localRegistry: ReactRenderRegistry = Object.freeze({
  Heading,
  Value: {
    kind: 'readiness',
    component: Value,
    isReady: valueReady,
    fallback: PendingValue,
  },
  Card,
});

Stream complete calculations

This preview admits only its three authored samples, their four computation names and declared argument values. The controls select from three bounded authored samples. A calculation waits until its expression and all arguments are complete, so a numeric or date prefix cannot briefly display an incorrect result. The copied display tree is frozen independently of the private parser.

projection.ts
import type {
  RenderElementData,
  RenderSpecData,
  RenderValue,
} from '@threadplane/react/render';
 
import type { JsonNode } from '@cacheplane/partial-json';
 
const TYPES = ['Heading', 'Card', 'Value'];
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 child(
  node: JsonNode | null | undefined,
  key: string
): JsonNode | undefined {
  return node?.type === 'object' ? node.children.get(key) : undefined;
}
 
function computed(
  input: unknown,
  complete: boolean,
  node?: JsonNode
): RenderValue | undefined {
  const expression = record(input);
  fields(expression, ['$computed', 'args']);
  // A complete-looking numeric/string value may still be an unfinished token.
  // Keep this private metadata attached to the exact expression path.
  if (!complete && node?.status !== 'complete') return undefined;
  const name = expression.$computed;
  const args = record(expression.args);
  if (name === 'multiply') {
    fields(args, ['a', 'b']);
    if (!((args.a === 7 && args.b === 6) || (args.a === 12 && args.b === 5)))
      invalid();
  } else if (
    name === 'uppercase' ||
    name === 'reverse' ||
    name === 'formatDate'
  ) {
    fields(args, ['value']);
    const values =
      name === 'uppercase'
        ? ['hello world', 'computed functions']
        : name === 'reverse'
        ? ['streaming']
        : ['2024-06-15T12:00:00Z', '2025-01-01T00:00:00Z'];
    if (typeof args.value !== 'string' || !values.includes(args.value))
      invalid();
  } else invalid();
  const owned = Object.create(null) as Record<string, RenderValue>;
  for (const [key, value] of Object.entries(args))
    Object.defineProperty(owned, key, { value, enumerable: true });
  return Object.freeze({ $computed: name, args: Object.freeze(owned) });
}
 
function project(
  input: unknown,
  complete: boolean,
  metadata?: JsonNode | null
): 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 === 'Value'
        ? ['label', 'value']
        : ['content']
    );
    const props = Object.create(null) as Record<string, RenderValue>;
    for (const [prop, value] of Object.entries(rawProps)) {
      if (value === undefined && !complete) continue;
      let copied: RenderValue | undefined;
      if (element.type === 'Value' && prop === 'value') {
        const node = child(
          child(child(child(metadata, 'elements'), key), 'props'),
          'value'
        );
        copied = computed(value, complete, node);
        if (copied !== undefined) textSize += JSON.stringify(copied).length;
      } else {
        if (typeof value !== 'string') invalid();
        textSize += value.length;
        copied = value;
      }
      if (textSize > MAX_TEXT) invalid();
      if (copied !== undefined)
        Object.defineProperty(props, prop, { value: copied, 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,
  metadata: JsonNode | null
): RenderSpecData | null {
  return project(input, false, metadata);
}
 
/** Admit a complete authored local sample before any playback begins. */
export function validateCompleteSpec(input: unknown): RenderSpecData {
  return project(input, true) ?? invalid();
}

Seeking rebuilds the parser from the selected prefix. Pause, sample changes, Reset and page exit cancel the current playback generation. Completion requires the parser’s final copied output to match the validated authored 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, parser.root);
      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-computed-functions-react
npx nx e2e cockpit-render-computed-functions-react

The isolated build installs local React, core and content package artifacts plus root-locked presentation dependencies. Browser coverage exercises all computed samples, partial playback, rewind, cancellation and narrow screens. Select Angular to read and run the existing Angular implementation.

Looking for something specific?