Do AI Agents Need a Semantic Layer?Livestream August 26

Skip to main content

useSQLQuery hook

The useSQLQuery hook is a React hook that runs SQL queries against MotherDuck from within a Dive. It handles loading states and error reporting so your component can focus on rendering data.

Import

import { useSQLQuery } from "@motherduck/react-sql-query";

Syntax

const { data, isLoading, isError, error, exportAs } = useSQLQuery(sql, options);

Parameters

sql

TypeRequired
stringYes

A SQL query string to run against MotherDuck. The Dive runtime returns rows as objects.

Use fully qualified, double-quoted table names in your queries ("database"."schema"."table") to avoid issues when the Dive runs outside your current database context.

options

PropertyTypeDefaultDescription
enabledbooleantrueSet to false to skip query execution. Useful when a query depends on user input that isn't available yet.

Return value

PropertyTypeDescription
dataarray or undefinedQuery result as an array of row objects. undefined while loading.
isLoadingbooleantrue while the query is running
isErrorbooleantrue if the query failed
errorError or nullError object if the query failed
exportAs(options) => Promise<void>Exports this query as a file
warning

data is the row array directly — there is no data.rows wrapper. Always guard against undefined:

const rows = Array.isArray(data) ? data : [];

Numeric values

DuckDB returns BIGINT, HUGEINT, and DECIMAL as JavaScript BigInt or special objects, not number. These crash if rendered in JSX or used with .toFixed(). Define this helper at the top of every Dive and wrap all numeric values:

const N = (v) => (v != null ? Number(v) : 0);

Examples

Basic query

import { useSQLQuery } from "@motherduck/react-sql-query";

const N = (v) => (v != null ? Number(v) : 0);

export default function Dive() {
const { data, isLoading } = useSQLQuery(`
SELECT category, SUM(amount) AS total
FROM "my_db"."main"."sales"
GROUP BY ALL
`);

if (isLoading) return <div>Loading...</div>;

const rows = Array.isArray(data) ? data : [];

return (
<ul>
{rows.map((row) => (
<li key={row.category}>
{row.category}: ${N(row.total).toLocaleString()}
</li>
))}
</ul>
);
}

Conditional queries with enabled

Skip a query until a user selection is available:

const [selected, setSelected] = useState(null);

const { data: details } = useSQLQuery(`
SELECT * FROM "my_db"."main"."products"
WHERE category = '${selected}'
`, { enabled: !!selected });

Multiple independent queries

Each useSQLQuery call loads independently. Render the page layout immediately and show inline placeholders per section instead of a single loading spinner:

const summary = useSQLQuery(`
SELECT COUNT(*) AS total, SUM(revenue) AS revenue
FROM "my_db"."main"."orders"
`);

const monthly = useSQLQuery(`
SELECT strftime(date_trunc('month', order_date), '%Y-%m') AS month,
SUM(revenue) AS revenue
FROM "my_db"."main"."orders"
GROUP BY 1 ORDER BY 1
`);

return (
<div>
{summary.isLoading
? <div className="h-12 w-24 bg-gray-200 animate-pulse rounded" />
: <p>{N(summary.data?.[0]?.revenue)}</p>
}
{monthly.isLoading
? <div className="bg-gray-100 animate-pulse rounded" style={{height: 250}} />
: <MyChart data={monthly.data} />
}
</div>
);

Export query results

Use exportAs() when you want to export the same SQL query used by a useSQLQuery() hook. Exports must start from a user action, such as a button click.

import { useSQLQuery } from "@motherduck/react-sql-query";

export default function Dive() {
const orders = useSQLQuery(`
SELECT *
FROM "my_db"."main"."orders"
ORDER BY order_date DESC
`);

return (
<button
type="button"
disabled={orders.isLoading}
onClick={() =>
orders.exportAs({
format: "csv",
title: "Orders",
filename: "orders",
})
}
>
Export orders
</button>
);
}

Use useExport() when the export should run a different SQL query than the rows rendered on the page:

import { useExport } from "@motherduck/react-sql-query";

export default function DiveExportButton() {
const { exportQuery } = useExport();

return (
<button
type="button"
onClick={() =>
exportQuery({
sql: `
SELECT customer_id, SUM(amount) AS total_amount
FROM "my_db"."main"."orders"
GROUP BY ALL
ORDER BY total_amount DESC
`,
format: "parquet",
title: "Customer totals",
filename: "customer-totals",
})
}
>
Export customer totals
</button>
);
}

Supported formats are csv, json, parquet, and xlsx. The filename value is a base name; MotherDuck adds the file extension. You can pass DuckDB COPY writer options under csv, json, parquet, or xlsx:

orders.exportAs({
format: "xlsx",
filename: "orders",
xlsx: {
sheet: "Orders",
header: true,
},
});

Exports run with DuckDB COPY TO, not from the rows already materialized in React. This means an export can contain more rows than the Dive renders. Export SQL must be read-only.

Tips

  • Format dates in SQL, not JavaScript. Use DuckDB's strftime() to format dates and timestamps as strings. DuckDB date types are returned as special objects that don't render in JSX.
  • Fill time series gaps in SQL. Recharts does not interpolate missing time periods. Use generate_series with a LEFT JOIN to produce a continuous date spine.
  • Dollar-quoted strings. When passing Dive content through SQL (for example, with MD_CREATE_DIVE), use dollar-quoted string literals to avoid escaping issues with nested quotes.