import { Code } from "@/components/code"; export const metadata = { title: "Streaming | json-render", }; export default function StreamingPage() { return (

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"}}}`}

useUIStream Hook

The hook handles parsing and state management:

{`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 }); }`}

Patch Operations (RFC 6902)

SpecStream uses{" "} RFC 6902 JSON Patch {" "} operations:

Path Format

Paths use a key-based format for elements:

{`/root -> Root element /root/children -> Children of root /elements/card-1 -> Element with key "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(); }`}

Progressive Rendering

The Renderer automatically updates as the spec changes:

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

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 (
); }`}

Low-Level SpecStream API

For custom streaming implementations, use the SpecStream compiler directly:

{`import { createSpecStreamCompiler } from '@json-render/core'; // Create a compiler for your spec type const compiler = createSpecStreamCompiler(); // Process streaming chunks from AI async function processStream(reader: ReadableStreamDefaultReader) { while (true) { const { done, value } = await reader.read(); if (done) break; const { result, newPatches } = compiler.push(value); 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":{"type":"Card"}} {"op":"add","path":"/root/props","value":{"title":"Hello"}}\`; const spec = compileSpecStream(jsonl); // { root: { type: "Card", props: { title: "Hello" } } }`}
); }