# Example: Partial row stream

Grow one row's content cell in place with connectPartialStream, covering both the seeded-row and createRow/onIssue patterns.

Source: https://pretable.ai/examples/partial-row-stream.md

```tsx PartialRowGrid.tsx
"use client";

import { connectPartialStream } from "@pretable/stream-adapter";
import { PretableSurface, useDisposeOnUnmount } from "@pretable/react";
import { createLocalRowModel } from "@pretable/core";
import { useEffect, useMemo } from "react";

import { columns, type MessageRow } from "./columns";
import { scriptedFirstReply, scriptedSecondReply } from "./scripted-partials";

export function PartialRowGrid() {
  const rowModel = useMemo(
    () =>
      createLocalRowModel({
        // "msg-1" is seeded here, before any stream connects — the "seed
        // the row first" pattern. connectPartialStream never creates a
        // row on its own; without a row to find, every partial for an
        // unseeded id is reported through onIssue instead of applied.
        rows: [{ id: "msg-1", role: "assistant", content: "", tokens: 0 }],
        columns,
        getRowId: (row) => row.id,
      }),
    [],
  );

  useEffect(() => {
    // "msg-1" already exists, so this connection only ever patches it.
    const seeded = connectPartialStream(rowModel, scriptedFirstReply(), {
      rowId: "msg-1",
    });

    // "msg-2" does not exist yet. The first partial for it is reported
    // through onIssue as an "unknown-update-id" issue; createRow then
    // turns that partial's accumulated changes into the new row. Every
    // later partial for "msg-2" is a normal update from there on.
    const created = connectPartialStream(rowModel, scriptedSecondReply(), {
      rowId: "msg-2",
      onIssue: (issue) => {
        console.warn(`[partial-row-stream] ${issue.code}: ${issue.rowId}`);
      },
      createRow: (partial, id): MessageRow => ({
        id,
        role: "assistant",
        content: partial.content ?? "",
        tokens: partial.tokens ?? 0,
      }),
    });

    return () => {
      seeded.dispose();
      created.dispose();
    };
  }, [rowModel]);

  // NOT `useEffect(() => () => rowModel.dispose())`: StrictMode rehearses an
  // unmount in dev, `useMemo` hands the same model back to the remount, and the
  // grid then renders nothing at all. `useDisposeOnUnmount` defers the disposal
  // by a microtask so a remount can cancel it.
  useDisposeOnUnmount(rowModel);

  return (
    <PretableSurface
      ariaLabel="Partial row stream"
      model={rowModel}
      viewportHeight={220}
    />
  );
}
```

```ts columns.ts
import { createColumnHelper } from "@pretable/core";

export interface MessageRow {
  id: string;
  role: "user" | "assistant";
  content: string;
  tokens: number;
}

const column = createColumnHelper<MessageRow>();

export const columns = [
  column.accessor("role", { type: "enum", header: "Role" }),
  column.accessor("content", { type: "text", header: "Content" }),
  column.accessor("tokens", { type: "number", header: "Tokens" }),
] as const;
```

```ts scripted-partials.ts
import type { MessageRow } from "./columns";

/** Partials for "msg-1", a row the demo seeds before the stream connects. */
export const FIRST_REPLY =
  "Hello! This row already existed, so the stream only ever patches it.";

/** Partials for "msg-2", a row that does not exist until createRow builds it. */
export const SECOND_REPLY =
  "This row did not exist yet. createRow made it from the first partial.";

export const INTERVAL_MS = 140;

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Yields the growing prefix of `text`, one character at a time. Each
 * partial carries the *whole* value so far, not just the newly-added
 * slice — connectPartialStream applies `changes` as-is, it doesn't diff
 * against the previous partial, so the source is responsible for the
 * accumulation (the same shape a token-streamed LLM response takes once
 * you've concatenated its deltas).
 */
async function* growingContent(
  text: string,
  intervalMs: number,
): AsyncIterable<Partial<MessageRow>> {
  for (let i = 1; i <= text.length; i++) {
    yield { content: text.slice(0, i), tokens: i };
    await sleep(intervalMs);
  }
}

/** Drives "msg-1": a row that was seeded before this stream connects. */
export function scriptedFirstReply(): AsyncIterable<Partial<MessageRow>> {
  return growingContent(FIRST_REPLY, INTERVAL_MS);
}

/** Drives "msg-2": a row created on the fly by `createRow`. */
export function scriptedSecondReply(): AsyncIterable<Partial<MessageRow>> {
  return growingContent(SECOND_REPLY, INTERVAL_MS);
}
```
