Dashboard: Async Report (useReportQuery) Pattern
Overview
Some report layouts run long-running queries asynchronously on the server. The platform provides a useReportQuery global hook that polls for completion. The widget waits for the Completed status before rendering data, and shows an error on Failed.
When to Use
- The report is configured as an async report in the Protrak admin (not a simple pre-aggregated
totalDatareport) - Report data arrives as a structured JSON object (
reportJson.data) rather than atotalDataarray - Widget needs to handle a loading state while the platform processes the report
Applicable Layout Types
| Target | Notes |
|---|---|
DashboardLayout | Primary target — platform injects useReportQuery and a config variable |
ReportLayout | Also supported |
Pattern Structure
- Call
useReportQuery(config, 'TypeName')—configis a global variable injected by the platform's report configuration (not defined in the widget) - Track report status via
reportState.data.statusin auseEffect - Render a
Spinnerwhilestatus !== 'Completed' - Render an
ErrorModalonstatus === 'Failed' - On
Completed, access data viareportState.data.reportJson.data
Code Example
// NOTE: 'config' is a global variable injected by the report layout configuration.
// It is NOT defined inside the widget function.
// NOTE: 'useReportQuery' is a global hook injected by the platform for report layouts.
function AnyWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Container, Spinner, ErrorModal } = protrakComponents;
const [reportStatus, setReportStatus] = React.useState(null);
// useReportQuery polls the server until the report completes
const { reportState } = useReportQuery(config, 'Element');
React.useEffect(() => {
if (reportState.data) {
setReportStatus(reportState.data.status);
}
}, [reportState.data]);
if (reportStatus === 'Completed') {
const rawData = reportState.data?.reportJson?.data || [];
if (rawData.length === 0) {
return (
<Container>
<p>No data available.</p>
</Container>
);
}
return <RenderTable data={processData(rawData)} />;
}
if (reportStatus === 'Failed') {
return (
<Container>
<ErrorModal message="Error while fetching report data..." />
</Container>
);
}
// Still loading (status is null, 'Pending', or 'Processing')
return (
<Container>
<Spinner />
</Container>
);
}
// Helper: transform raw report rows into display-ready data
function processData(rawData) {
// Apply any grouping, aggregation, or transformation here
return rawData.reduce((acc, row) => {
// ... aggregate logic ...
return acc;
}, {});
}
// Sub-component: renders the processed data
const RenderTable = ({ data }) => {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Dropdown } = protrakComponents;
const [filter, setFilter] = React.useState(null);
const rows = Object.values(data);
const filtered = filter ? rows.filter((r) => r.category === filter) : rows;
return (
<div>
<Dropdown
picklistOptions={[
...new Set(
rows.map((r) => ({ label: r.category, value: r.category }))
),
]}
onValueChange={setFilter}
/>
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
{/* ... render filtered rows ... */}
</table>
</div>
);
};
Key Differences vs totalData Pattern
| Aspect | totalData Pattern | useReportQuery Pattern |
|---|---|---|
| Data source | pageContext.totalData | reportState.data.reportJson.data |
| Loading state | No loading — data arrives synchronously | Must handle loading/failed states |
| Report configuration | Declarative in admin | Async report config; config variable injected |
| Filter dropdowns | Widget manages filters | Widget manages filters |
| Sub-components | Optional | Common — separate RenderTable component |
Notes
useReportQueryandconfigare globally injected by the platform's report layout infrastructure. Do not attempt to define or import them.- Sub-components (like
RenderTable) defined inside the widget's.jsfile are fine — they are within the function scope and do not pollute the global scope. Alternatively, define them as inner functions. - Avoid defining variables like
elementTypes,levels,grandTotalat module scope — useuseReforuseMemoinside the component instead.
Real Examples
AnyWidget.js— Buildcast, DevinciPrecastPOCPlannedVsActualDailyReportWidget.js— Buildcast, DevinciPrecastPOCPlannedVsActualDailyReportForPrestressedTypeElements.js— Buildcast, DevinciPrecastPOCPlannedVsActualDailyReportForNotPrestressedTypeElements.js— Buildcast, DevinciPrecastPOCProjectSummaryReport.js— Buildcast, DevinciPrecastPOC