Streaming

Progressively render UI as AI generates it.

SpecStream Format

json-render uses SpecStream, a JSONL-based streaming format where each line is a JSON patch operation that progressively builds your spec:

{"op":"add","path":"/root","value":"root"}
{"op":"add","path":"/elements/root","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","metric-2"]}}
{"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue"}}}
{"op":"add","path":"/elements/metric-2","value":{"type":"Metric","props":{"label":"Users"}}}

Patch Operations (RFC 6902)

SpecStream uses RFC 6902 JSON Patch operations:

  • add — Add a value at a path (creates or replaces for objects, inserts for arrays)
  • remove — Remove the value at a path
  • replace — Replace an existing value at a path
  • move — Move a value from one path to another (requires from)
  • copy — Copy a value from one path to another (requires from)
  • test — Assert that a value at a path equals the given value

Path Format

Paths follow JSON Pointer (RFC 6901) into the spec object:

/root                       -> Root element key (string)
/elements/card-1            -> Element with key "card-1"
/elements/card-1/props      -> Props of card-1
/elements/card-1/children   -> Children of card-1

Server-Side Setup

Ensure your API route streams properly:

import { streamText } from 'ai';
import { catalog } from '@/lib/catalog';

export async function POST(req: Request) {
  const { prompt } = await req.json();
  
  const result = streamText({
    model: 'anthropic/claude-haiku-4.5',
    system: catalog.prompt(),
    prompt,
  });

  // Return as a streaming response
  return result.toTextStreamResponse();
}

Low-Level SpecStream API

For custom or framework-agnostic streaming implementations, use the SpecStream compiler from @json-render/core directly:

import { createSpecStreamCompiler } from '@json-render/core';

// Create a compiler for your spec type
const compiler = createSpecStreamCompiler<MySpec>();
const decoder = new TextDecoder();

// Process streaming chunks from AI
async function processStream(reader: ReadableStreamDefaultReader<Uint8Array>) {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    // Decode the Uint8Array chunk to a string
    const chunk = decoder.decode(value, { stream: true });
    const { result, newPatches } = compiler.push(chunk);
    
    if (newPatches.length > 0) {
      // Update UI with partial result
      setSpec(result);
    }
  }
  
  // Get final compiled result
  return compiler.getResult();
}

One-Shot Compilation

For non-streaming scenarios, compile entire SpecStream at once:

import { compileSpecStream } from '@json-render/core';

const jsonl = `{"op":"add","path":"/root","value":"card-1"}
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Hello"},"children":[]}}`;

const spec = compileSpecStream<Spec>(jsonl);
// { root: "card-1", elements: { "card-1": { type: "Card", props: { title: "Hello" }, children: [] } } }

Usage with React

@json-render/react provides the useUIStream hook, which wraps the low-level compiler in a React-friendly API with state management, error handling, and abort support.

useUIStream Hook

import { useUIStream } from '@json-render/react';

function App() {
  const {
    spec,          // Current UI spec state
    isStreaming,   // True while streaming
    error,         // Any error that occurred
    send,          // Function to start generation
    clear,         // Function to reset spec and error
  } = useUIStream({
    api: '/api/generate',
    onComplete: (spec) => {},  // Optional: called when streaming completes
    onError: (error) => {},    // Optional: called when an error occurs
  });
}

Progressive Rendering

The Renderer automatically updates as the spec changes:

function App() {
  const { spec, isStreaming } = useUIStream({ api: '/api/generate' });

  return (
    <div>
      {isStreaming && <LoadingIndicator />}
      <Renderer spec={spec} registry={registry} loading={isStreaming} />
    </div>
  );
}

Aborting Streams

Calling send again automatically aborts the previous request. Use clear to reset the spec and error state:

function App() {
  const { isStreaming, send, clear } = useUIStream({
    api: '/api/generate',
  });

  return (
    <div>
      <button onClick={() => send('Create dashboard')}>
        Generate
      </button>
      <button onClick={clear}>Reset</button>
    </div>
  );
}

See the @json-render/react API reference for full useUIStream documentation.