# Example: Watching a rebuild

Grouping 150,000 rows cannot settle inside one animation frame, so status.kind cycles through rebuilding with a live completedRows/totalRows progress readout before returning to ready.

Source: https://pretable.ai/examples/headless-rebuild-progress.md

```tsx RebuildProgressDemo.tsx
"use client";

import { useCallback, useState, useSyncExternalStore } from "react";

import { createLocalRowModel } from "@pretable/core";
import { useDisposeOnUnmount } from "@pretable/react";

import { columns } from "./columns";
import { ORDER_COUNT, orders } from "./data";
import { RebuildProgress } from "./RebuildProgress";

const PREVIEW_ROWS = 8;

export function RebuildProgressDemo() {
  const [rowModel] = useState(() =>
    createLocalRowModel({ columns, rows: orders }),
  );
  useDisposeOnUnmount(rowModel);

  // Selecting `snapshot` (not the whole state) means this component bails
  // out on identity between rebuild slices — it only renders once, when the
  // grouping change actually lands. `RebuildProgress` above is the one
  // re-rendering on every slice in the meantime.
  const readSnapshot = useCallback(
    () => rowModel.getState().snapshot,
    [rowModel],
  );
  const snapshot = useSyncExternalStore(
    rowModel.subscribe,
    readSnapshot,
    readSnapshot,
  );

  const [grouped, setGrouped] = useState(false);

  // A GROUPING change, not a filter or a sort: both of those settle
  // synchronously on ungrouped data (the sort fast path and the filter fast
  // path each require `rowGroups.length === 0`), so neither could
  // demonstrate the progress readout anymore. Grouping never takes a fast
  // path — it always rebuilds cooperatively — which is exactly why it is the
  // vehicle here.
  const toggleGrouped = () => {
    const next = !grouped;
    setGrouped(next);
    rowModel.setQuery({
      ...snapshot.query,
      rowGroups: next ? [{ columnId: "region" }] : [],
    });
  };

  return (
    <div>
      <button type="button" onClick={toggleGrouped}>
        {grouped
          ? "Ungroup"
          : `Group ${ORDER_COUNT.toLocaleString()} orders by region`}
      </button>
      <RebuildProgress rowModel={rowModel} />
      <p style={{ fontSize: 13 }}>
        {snapshot.visibleRowCount.toLocaleString()} rows indexed — showing the
        first {PREVIEW_ROWS}
      </p>
      <table>
        <thead>
          <tr>
            {columns.map((c) => (
              <th key={c.id} scope="col">
                {c.header ?? c.id}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {snapshot
            .range(0, Math.min(PREVIEW_ROWS, snapshot.visibleRowCount))
            .map((entry) =>
              entry.kind === "data" ? (
                <tr key={entry.rowId}>
                  {columns.map((c) => (
                    <td key={c.id}>{String(c.accessor(entry.row))}</td>
                  ))}
                </tr>
              ) : (
                <tr key={entry.groupId}>
                  <td colSpan={columns.length}>
                    {String(entry.value)} ({entry.childCount})
                  </td>
                </tr>
              ),
            )}
        </tbody>
      </table>
    </div>
  );
}
```

```tsx RebuildProgress.tsx
"use client";

import { useCallback, useSyncExternalStore } from "react";

import type { PretableRowModel } from "@pretable/core";

/**
 * Subscribes to `status` ONLY — never to `snapshot`. That isolation is the
 * whole point: a rebuild over 150,000 rows publishes dozens of slices, and
 * keeping this readout in its own component means each slice re-renders
 * this one paragraph, not the (much larger) table underneath it.
 *
 * Selecting a STRING keeps `useSyncExternalStore` cheap between slices —
 * see the note on the smaller custom-renderer example for why the object
 * itself, or even `status.kind` alone, would be the wrong thing to select.
 */
export function RebuildProgress<
  TRow extends object,
  TRowId extends string | number,
  TColumns,
>({ rowModel }: { rowModel: PretableRowModel<TRow, TRowId, TColumns> }) {
  const readProgressText = useCallback(() => {
    const { status } = rowModel.getState();
    if (status.kind !== "rebuilding") return status.kind;
    const pct =
      status.totalRows === 0
        ? 0
        : Math.min(
            100,
            Math.round((status.completedRows / status.totalRows) * 100),
          );
    return `rebuilding:${pct}`;
  }, [rowModel]);

  const progressText = useSyncExternalStore(
    rowModel.subscribe,
    readProgressText,
    readProgressText,
  );

  const label = progressText.startsWith("rebuilding:")
    ? `Rebuilding… ${progressText.slice("rebuilding:".length)}%`
    : progressText === "ready"
      ? "Ready."
      : progressText;

  return (
    <p role="status" aria-live="polite" style={{ fontSize: 13 }}>
      {label}
    </p>
  );
}
```

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

import type { Order } from "./data";

const column = createColumnHelper<Order>();

export const columns = [
  column.accessor("customer", { type: "text", header: "Customer" }),
  column.accessor("region", { type: "text", header: "Region" }),
  column.accessor("amount", { type: "number", header: "Amount" }),
] as const;
```

```ts data.ts
export interface Order {
  id: string;
  customer: string;
  region: string;
  amount: number;
}

const REGIONS = ["north", "south", "east", "west", "central"];

// Deliberately large and deterministic (no Math.random): big enough that a
// grouping change cannot settle inside one animation frame, so the rebuild
// really does publish multiple `rebuilding` slices instead of jumping
// straight to `ready` — see the note on the smaller custom-renderer example.
export const ORDER_COUNT = 150_000;

export const orders: Order[] = Array.from({ length: ORDER_COUNT }, (_, i) => ({
  id: `order-${i}`,
  customer: `Customer ${i % 5000}`,
  region: REGIONS[i % REGIONS.length]!,
  amount: ((i * 2654435761) % 100000) / 100,
}));
```
