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
}Auto-ingest (recommended)
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 yetMixing 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.