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
| Type | Required |
|---|---|
string | Yes |
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
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Set to false to skip query execution. Useful when a query depends on user input that isn't available yet. |
Return value
| Property | Type | Description |
|---|---|---|
data | array or undefined | Query result as an array of row objects. undefined while loading. |
isLoading | boolean | true while the query is running |
isError | boolean | true if the query failed |
error | Error or null | Error object if the query failed |
exportAs | (options) => Promise<void> | Exports this query as a file |
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_serieswith aLEFT JOINto 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.
Related
- Dives SQL functions — Manage Dives with SQL
useDiveStatehook — Store shareable filters, sorting, and view state in the Dive URL- Creating visualizations with Dives — How-to guide
- Embedding Dives in your web application — Handle embedded Dive sessions and exports
- MCP Server — Create Dives through AI assistants