useQuery Hook
The useQuery hook is how your dashboard fetches and interacts with data. It connects to queries you've registered in the Overscore Hub, handles caching automatically, and gives you the ability to run local SQL against cached results.
Setup: OverscoreProvider
Before using useQuery, wrap your app with the OverscoreProvider. This is typically done in your root component or main.tsx:
import { OverscoreProvider } from "@overscore/client";
function App() {
return (
<OverscoreProvider>
<Dashboard />
</OverscoreProvider>
);
}
The provider reads your API key and project configuration from environment variables automatically. No props required.
Basic Usage
Import the hook and call it with the name of a query registered in the Hub:
import { useQuery } from "@overscore/client";
function RevenueChart() {
const { data, loading, error } = useQuery("monthly_revenue");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data.map((row) => (
<div key={row.month}>
{row.month}: ${row.revenue.toLocaleString()}
</div>
))}
</div>
);
}
The query name (e.g., "monthly_revenue") must match a query you've created in the Hub for your dashboard.
Return Fields
The useQuery hook returns an object with four fields:
data
const { data } = useQuery("monthly_revenue");
// data: Array<Record<string, any>> | null
An array of row objects, where each key corresponds to a column from the query result. Returns null until the first successful fetch.
loading
const { loading } = useQuery("monthly_revenue");
// loading: boolean
true while the query is being fetched or the cache is being loaded. false once data is available or an error occurs.
error
const { error } = useQuery("monthly_revenue");
// error: Error | null
An Error object if the query failed, otherwise null. Common causes include invalid query names, expired API keys, or BigQuery permission errors.
query
const { query } = useQuery("monthly_revenue");
// query: (sql: string) => Promise<Array<Record<string, any>>>
A function that lets you run arbitrary SQL against the cached query results using DuckDB-WASM. This is the key to local filtering and aggregation — see the section below.
Caching Behavior
Overscore uses a two-layer caching system to keep dashboards fast:
-
Server-side caching — when a query is executed against BigQuery, the results are serialized as a Parquet file and stored on the server.
-
Client-side caching via DuckDB-WASM — on subsequent page loads, the Parquet file is downloaded and loaded into a DuckDB-WASM instance running in the browser. This means data is available almost instantly without re-querying BigQuery.
TTL (Time to Live)
Each query has a configurable TTL that controls how long cached results are considered fresh. When the TTL expires, the next request will trigger a fresh query to BigQuery and update the cache.
- Default TTL is set per query in the Hub
- During the TTL window, all requests are served from the Parquet cache
- After TTL expiry, a background refresh runs — users still see the cached data while the new results load
This means your dashboards feel fast even with large datasets, and BigQuery costs stay low because you're not re-running expensive queries on every page load.
Local SQL Filtering with query()
The query() function is one of the most powerful features in Overscore. It lets you run SQL against your cached data entirely in the browser using DuckDB-WASM — no network request, no BigQuery cost, instant results.
function TopCustomers() {
const { data, query } = useQuery("customer_metrics");
const [topCustomers, setTopCustomers] = useState(null);
useEffect(() => {
if (data) {
query("SELECT * FROM data ORDER BY revenue DESC LIMIT 10")
.then(setTopCustomers);
}
}, [data, query]);
if (!topCustomers) return <div>Loading...</div>;
return (
<ul>
{topCustomers.map((c) => (
<li key={c.customer_id}>
{c.name} — ${c.revenue.toLocaleString()}
</li>
))}
</ul>
);
}
The cached data is available as a table called data in your SQL. You can use the full power of DuckDB SQL:
// Aggregation
await query("SELECT region, SUM(revenue) as total FROM data GROUP BY region");
// Filtering
await query("SELECT * FROM data WHERE status = 'active' AND created_at > '2025-01-01'");
// Window functions
await query(`
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) as rank
FROM data
`);
// Joins across multiple cached queries are not supported —
// each query() call operates on its own cached dataset.
This is ideal for building interactive filters, search, pagination, and drill-downs without hitting your data warehouse on every interaction.
Full Example
Here's a complete component that fetches data, shows a loading state, and lets users filter with a search box:
import { useState, useEffect } from "react";
import { useQuery } from "@overscore/client";
function OrdersTable() {
const { data, loading, error, query: runQuery } = useQuery("recent_orders");
const [search, setSearch] = useState("");
const [filtered, setFiltered] = useState(null);
useEffect(() => {
if (!data) return;
if (search.length === 0) {
setFiltered(data);
} else {
runQuery(
`SELECT * FROM data WHERE customer_name ILIKE '%${search}%'`
).then(setFiltered);
}
}, [data, search, runQuery]);
if (loading) return <div>Loading orders...</div>;
if (error) return <div>Failed to load orders: {error.message}</div>;
return (
<div>
<input
type="text"
placeholder="Search customers..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<table>
<thead>
<tr>
<th>Order ID</th>
<th>Customer</th>
<th>Amount</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{filtered?.map((row) => (
<tr key={row.order_id}>
<td>{row.order_id}</td>
<td>{row.customer_name}</td>
<td>${row.amount}</td>
<td>{row.order_date}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Next Steps
- Core Concepts — understand projects, queries, and connections
- BigQuery Setup — connect your data warehouse
- Deploying — ship your dashboard to production