DuckVizBeta
Guides

Datasets Mode

Use Explorer with pre-ingested DuckDB tables instead of file uploads.

By default, the Explorer shows a file-upload interface. When you pass the datasets prop, it skips file upload entirely and renders a flat sidebar with your pre-ingested tables. This is the recommended pattern when your data comes from an API or database rather than user-uploaded files.

When to use datasets mode

  • Your app fetches data from a backend API
  • You have structured data from a database or service
  • You want to pre-populate the Explorer with specific tables
  • You don't want users to upload their own files

Two flavors: auto-ingest vs. pre-ingested

The datasets prop accepts two different shapes:

Auto-ingest (DuckvizDataset[]) — hand Explorer the row data directly and it will ingest into DuckDB on mount. Simpler for most apps:

interface DuckvizDataset {
  name: string;                     // Display name; also derives tableName
  data: Record<string, unknown>[];  // Row data to ingest
  tableName?: string;               // Optional explicit override
}

Pre-ingested (ExplorerDataset[]) — reference tables you already ingested yourself:

interface ExplorerDataset {
  id: string;        // Unique identifier
  name: string;      // Display name in the sidebar
  tableName: string; // Must match an already-ingested DuckDB table
}

Pass row data directly — Explorer handles ingestion for you:

"use client";

import { Explorer } from "@duckviz/explorer";
import type { DuckvizDataset } from "@duckviz/db";

export default function DataExplorer({ orders, customers }: Props) {
  const datasets: DuckvizDataset[] = [
    { name: "Orders", data: orders },       // → t_orders
    { name: "Customers", data: customers }, // → t_customers
  ];

  return (
    <Explorer
      datasets={datasets}
      dashboards={[]}
      onAddWidgetToDashboard={() => ({ ok: true })}
      onCreateDashboard={(name) => crypto.randomUUID()}
      authenticated={true}
    />
  );
}

Tables are dropped from DuckDB when Explorer unmounts. Pass dropTablesOnUnmount={false} to keep them.

Pre-ingested tables

Use this shape when you need explicit control over ingestion (chunked streaming, custom SQL, migration logic, etc.):

1. Ingest data into DuckDB

const db = useDuckDB();

// From an API response
const res = await fetch("/api/orders");
const { orders } = await res.json();
await db.ingest({ rows: orders, tableName: "t_orders" });

// From multiple sources
await db.ingest({ rows: customers, tableName: "t_customers" });
await db.ingest({ rows: products, tableName: "t_products" });

2. Create dataset descriptors

Map your ingested tables to ExplorerDataset objects:

const datasets: ExplorerDataset[] = [
  { id: "orders",    name: "Orders",    tableName: "t_orders" },
  { id: "customers", name: "Customers", tableName: "t_customers" },
  { id: "products",  name: "Products",  tableName: "t_products" },
];

3. Pass to Explorer

<Explorer
  datasets={datasets}
  dashboards={dashboards}
  onAddWidgetToDashboard={handleAddWidget}
  onCreateDashboard={handleCreateDashboard}
  authenticated={true}
/>

Full example (pre-ingested)

"use client";

import { useEffect, useRef, useState } from "react";
import { useDuckDB } from "@duckviz/db";
import { Explorer, type ExplorerDataset } from "@duckviz/explorer";

export default function DataExplorer() {
  const db = useDuckDB();
  const [datasets, setDatasets] = useState<ExplorerDataset[] | null>(null);
  const seeded = useRef(false);

  useEffect(() => {
    if (db.loading || db.restoring || seeded.current) return;
    seeded.current = true;

    (async () => {
      // Skip seeding if tables already exist (from persistence)
      const exists = await db.tableExists("t_orders");
      if (!exists) {
        const res = await fetch("/api/orders");
        const { orders } = await res.json();
        await db.ingest({ rows: orders, tableName: "t_orders" });
      }

      setDatasets([
        { id: "orders", name: "Orders", tableName: "t_orders" },
      ]);
    })();
  }, [db]);

  if (!datasets) return <p>Loading data...</p>;

  return (
    <Explorer
      datasets={datasets}
      dashboards={[]}
      onAddWidgetToDashboard={() => ({ ok: true })}
      onCreateDashboard={(name) => crypto.randomUUID()}
      authenticated={true}
    />
  );
}

Key patterns (pre-ingested flow)

These only apply to the pre-ingested path — auto-ingest handles all of this for you.

Check before seeding

Always check tableExists() before ingesting. When persistence is enabled on DuckvizDBProvider, tables survive browser refreshes — you don't want to re-fetch and re-ingest data unnecessarily.

Use a ref to prevent double-seeding

React's Strict Mode runs effects twice in development. Use a useRef flag to ensure the seeding logic only runs once:

const seeded = useRef(false);

useEffect(() => {
  if (seeded.current) return;
  seeded.current = true;
  // ... seed logic
}, [db]);

Wait for DuckDB to be ready

Always check db.loading and db.restoring before using any DuckDB methods:

if (db.loading || db.restoring) return; // DuckDB not ready yet

Mixing datasets with file upload

The datasets prop is all-or-nothing:

  • With datasets: flat sidebar, no file upload UI
  • Without datasets: full file-upload experience

If you need both, consider putting file-upload and dataset views on separate pages/tabs.