# React example

A complete component that opens a Space, paginates its samples, and renders thumbnails via `blobUrlFor`. ~100 lines, no extra deps beyond React.

## Setup

```bash
pnpm add @dreamlake/dreamdb react react-dom
```

## The component

```tsx

const SPACE_URI =
  "https://vortex-test-20260518.s3.us-east-1.amazonaws.com/refs/imagenet-10k-clip";

export default function ImageGrid() {
  const [space, setSpace] = useState(null);
  const [samples, setSamples] = useState<Sample[]>([]);
  const [page, setPage] = useState(0);
  const pageSize = 48;

  useEffect(() => {
    Space.fromUri(SPACE_URI).then(async (sp) => {
      setSpace(sp);
      const { samples } = await sp.samples();
      setSamples(samples);
    });
  }, []);

  const visible = useMemo(
    () => samples.slice(page * pageSize, (page + 1) * pageSize),
    [samples, page],
  );

  if (!space) return <p>Opening Space…</p>;
  return (
    <div>
      <header className="mb-2 flex items-center gap-3 text-sm">
        <span>{samples.length} samples</span>
        <button
          onClick={() => setPage((p) => Math.max(0, p - 1))}
          disabled={page === 0}
        >
          prev
        </button>
        <span>page {page + 1}</span>
        <button
          onClick={() => setPage((p) => p + 1)}
          disabled={(page + 1) * pageSize >= samples.length}
        >
          next
        </button>
      </header>
      <div className="grid grid-cols-6 gap-1">
        {visible.map((s) => (
          
        ))}
      </div>
    </div>
  );
}

function Thumb({ sample }: { sample: Sample }) {
  const [url, setUrl] = useState<string | null>(null);

  useEffect(() => {
    let revoked = false;
    let resolved: string | null = null;
    blobUrlFor(sample, "image").then((u) => {
      if (revoked || !u) return;
      resolved = u;
      setUrl(u);
    });
    return () => {
      revoked = true;
      if (resolved) revokeBlobUrl(resolved);
    };
  }, [sample]);

  return (
    <div className="aspect-square bg-neutral-100">
      {url && (
        <img
          src={url}
          alt={`anchor ${sample.anchor}`}
          className="h-full w-full object-cover"
        />
      )}
    </div>
  );
}
```

## Things to notice

- **One Space per page.** `Space.fromUri` fetches the ref + the Manifest; cache that in state, don't re-open per render.
- **Pagination is client-side.** `samples()` returns the whole materialized list — the SDK's eager fan-out. For >100K samples switch to `Dataset.iterStream` server-side or `space.itemsInRange` for windowed reads.
- **Each thumb has its own `useEffect`.** The cleanup function revokes the URL when the sample changes or the component unmounts. Without that you leak bytes.
- **`revoked` flag.** Handles the late-arriving fetch (resolved after unmount) so you don't try to revoke a URL that was never published.

## Going further

The full demo app at `dreamdb-workspace/demo/` shows:
- IntersectionObserver-based lazy loading
- React Query for content-addressed caching (`staleTime: Infinity` on Manifest hashes; finite on mutable refs)
- Zustand for the bucket-list state with `localStorage` persistence
- Tailwind v4 styling
- A semantic-search bar (see [Semantic search](/typescript-sdk-semantic-search.md))
