# 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:

```json
{"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](https://datatracker.ietf.org/doc/html/rfc6902) 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:

```bash
/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:

```typescript

  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:

```typescript

// 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:

```typescript

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

```tsx

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:

```tsx
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:

```tsx
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](/docs/api/react) for full `useUIStream` documentation.