MotherDuck Now Speaks Postgres! Our pg_endpoint is now live!Demo - April 21

Skip to main content

useSQLQuery hook

Preview
This feature is in preview and is subject to change.

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

TypeRequired
stringYes

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

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
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>
);

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.