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 } = useSQLQuery(sql, options);
Parameters
sql
| Type | Required |
|---|---|
string | Yes |
A SQL query string to run against MotherDuck. The query runs server-side and 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 |
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>
);
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
- Creating visualizations with Dives — How-to guide
- MCP Server — Create Dives through AI assistants