Running a query
In order to display data, we first have to fetch data… But how?
useQueryResult
Fortunately, @activeviam/atoti-ui-sdk
provides a React hook for this: useQueryResult.
Let's test it in Map.tsx
:
import {
+ useQueryResult,
WidgetPluginProps
} from "@activeviam/atoti-ui-sdk";
const Map: FC<WidgetPluginProps> = (props) => {
+ const {data, error, isLoading} = useQueryResult({
+ serverKey: "my-server",
+ queryId: "abc",
+ query: {
+ mdx: "SELECT FROM [Green-growth]"
+ }
+ })
+ console.log(isLoading, data);
return (
<div style={props.style}>
Hello World!
</div>
);
};
Now open the developer tools and refresh the application.
In the console there will be three log entries. This means that Map
was rendered three times.
Let's check out the logged state from each render:
isLoading
is false anddata
is undefined.isLoading
becomes true whiledata
remains undefined.isLoading
turns false again anddata
becomes an object (which will be explained below).
In other words, our widget now passes through three different states:
- The data did not start loading yet.
- The data is loading.
- The data is available.
In order to give the end-user as much feedback as possible, we should take the opportunity to represent each of these states.
But what exactly is going on? Let's dive deeper to find out.
Why a hook?
Fetching data is asynchronous: we can control when we ask the question, but not when (and even if) we get an answer.
The advantage of useQueryResult
is that it binds the lifecycle of the query to the lifecycle of the Map
Component:
- The query is registered when
Map
is mounted and unregistered when it is unmounted. Map
is rerendered when the query begins and finishes loading. This allows us to display these different states to the user.
Let's look into the arguments we need to pass to useQueryResult
in order to use it.
useQueryResult arguments
serverKey
serverKey
identifies which server to run the query against. This becomes especially important in applications that use more than one server.
In our example we entered serverKey: "my-server"
, but why did we pick that exact string: my-server
?
It turns out we chose this name when we set up our env.development.js
and env.production.js
files.
When setting up your application, it is important to remember that serverKey
will always be part of the content saved when users save dashboards to the Content Server. As soon as users start working on your application, they will begin persisting your chosen server keys.
It is better to avoid names susceptible to change during your project's lifecycle. For instance, "uat" and "prod" are poor choices, unless you specifically want your UAT dashboards to not be usable in production.
queryId
You guessed it: queryId
identifies our query.
It is particularly useful when other components need access to those query results.
If several Components call useQueryResult
with the same queryId
, then the query is only executed once. In this case, useQueryResult
's third argument, query
, can even be omitted. The Components in question would be subscribed to the query's results while explicitly signifying that something else is expected to control the query:
const AnotherComponentHearingThroughTheGrapeVine = () => {
const { data, error, isLoading } = useQueryResult({
serverKey: "my-server",
queryId: "abc",
// Because it does not provide `query`,
// this Component expects another Component to control the query.
});
return <div>Do not copy me, this is just an example.</div>;
};
You might think: wait, we cannot just hardcode queryId: "abc"
. And you would be right!
Our goal is not to share a single query across all instances of Map
. What if a user drags two of them into a single dashboard? Instead, we want every rendered widget to have its own query.
This can easily be done using the unique queryId
passed as a prop to our widget (in this case, by the dashboard, which makes sure each id is unique).
const {data, error, isLoading} = useQueryResult({
serverKey: "my-server",
- queryId: "abc",
+ queryId: props.queryId,
query: {
mdx: "SELECT FROM [Green-growth]"
}
})
But if all widgets have their own queryId
, then who benefits from reusing queries… Later on, we will see how "helper" Components (such as the fields) benefit from reusing a queryId
.
To quickly summarize useQueryResults
's arguments so far:
serverKey
lets us control which server we are talking to.queryId
lets us share the results of a query between multiple Components.
But what about the query itself? How do we ask for specific data?
query
Queries are written in MDX. SELECT FROM [Green-growth]
is a trivial example of MDX
.
Learning this querying language is outside the scope of this tutorial. But, if you are interested, this resource can help you get up to speed:
- Microsoft's MDX fundamentals.
Before moving on, let's implement a more specific query. This one returns the GDP per capita for each country and every year in our dataset:
const {data, error, isLoading} = useQueryResult({
serverKey: "my-server",
queryId: props.queryId,
query: {
- mdx: "SELECT FROM [Green-growth]"
+ mdx: `SELECT
+ [Countries].[Country].[Country_Name].Members ON ROWS,
+ Crossjoin(
+ [Green-growth].[Year].[Year].Members,
+ [Measures].[Real GDP per capita (USD).MEAN]
+ ) ON COLUMNS
+ FROM [Green-growth]`
}
})
If you look at the console and refresh your page, you will see that data
has taken a different value and that it has many more "cells".
Now that we have all the arguments of useQueryResult
, let's take a look at what it returns.
useQueryResult returned values
isLoading
isLoading
tells us whether or not we are loading data. This can happen when the widget first loads or when the user manually refreshes it.
It is a good opportunity to show the user that we are in a loading state by displaying a spinner or some other kind of progress component.
Let's go back to our fields and leverage isLoading
:
import React, { FC } from "react";
import { useQueryResult, WidgetPluginProps } from "@activeviam/atoti-ui-sdk";
+ import {Spin} from "antd";
export const Map: FC<WidgetPluginProps> = (props) => {
const {data, error, isLoading} = useQueryResult({
serverKey: "my-server",
queryId: "abc",
query: {
mdx: `SELECT
Crossjoin(
[Green-growth].[Year].[Year].Members,
[Measures].[Real GDP per capita (USD).MEAN]
) ON COLUMNS,
[Countries].[Country].[Country_Name].Members ON ROWS
FROM [Green-growth]`
}
});
+ if (isLoading) {
+ return <Spin />
+ }
You should now see this spinner (albeit very briefly) when the widget loads up:
error
Nobody likes errors, but if there is one thing worse than a loud error, it is a silent error that sneaks in unnoticed.
We might encounter an error if
- We mistakenly created an invalid query.
- A timeout was exceeded on the server.
- Any other unexpected event occurred before the response came in.
The best thing to do is notify the user that something went wrong and provide a course of action to recover from it.
Here is a naive, but efficient approach at it.
if (isLoading) {
return <Spin />
}
+ if (error) {
+ return <div>{error.stackTrace}</div>
+ }
Now let's deliberately break our query to trigger an error.
const {data, error, isLoading} = useQueryResult({
serverKey: "my-server",
queryId: "abc",
query: {
mdx: `SELECT
Crossjoin(
- [Green-growth].[Year].[Year].Members,
+ [Green-oops].[Year].[Year].Members,
[Measures].[Real GDP per capita (USD).MEAN]
) ON COLUMNS,
[Countries].[Country].[Country_Name].Members ON ROWS
FROM [Green-growth]`
}
});
Let's check the application again.
It worked! It's pretty scary-looking, but at least errors will not go unnoticed.
We strongly recommended creating friendlier, more actionable error messages before shipping to your users.
data
It is finally time to discuss data
and get a feeling for how we can use it in our application!
data
comes in the shape of what we call a CellSet.
The two attributes we use the most from data
are:
axes
which represents the members of the hierarchies we asked for.cells
which contains the values of the measures we chose, for each of these members.
Strong with this knowledge, we can explore data
through the basic HTML table below. (No need to copy this into our project.)
If you look back and forth between the rendered table and the underlying code, you will begin to pick up on how data
is structured.
We recommended taking at least few minutes with this.
if (!data) {
return null;
}
// HERE we use data.axes!
const [columnsAxis, rowsAxis] = data.axes;
const numberOfColumns = columnsAxis.positions.length;
return (
<table>
<tr>
<th />
{/* The header represents the members of the columns axis (the years). */}
{columnsAxis.positions.map((position, columnIndex) => (
<th key={columnIndex}>{position[0].captionPath[0]}</th>
))}
</tr>
{rowsAxis.positions.map((position, rowIndex) => {
const tableCells: JSX.Element[] = [];
// The first column represents the members of the rows axis (the countries).
tableCells.push(<td key={0}>{position[0].captionPath[2]}</td>);
for (let columnIndex = 0; columnIndex < numberOfColumns; columnIndex++) {
const cellIndex = rowIndex * numberOfColumns + columnIndex;
// HERE we use data.cells!
const dataCell = data.cells[cellIndex];
tableCells.push(
<td key={columnIndex + 1}>{dataCell?.formattedValue}</td>,
);
}
return <tr key={rowIndex}>{tableCells}</tr>;
})}
</table>
);
Congratulations for getting this far! We have learned how to retrieve data
using the Atoti JavaScript API.
On the next page, we will display data
in a more pleasing and useful way. 🧑🎨