Skip to main content

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.

Console log output

Let's check out the logged state from each render:

  1. isLoading is false and data is undefined.
  2. isLoading becomes true while data remains undefined.
  3. isLoading turns false again and data becomes an object (which will be explained below).

In other words, our widget now passes through three different states:

  1. The data did not start loading yet.
  2. The data is loading.
  3. 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.

serverKey

warning

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:

  1. serverKey lets us control which server we are talking to.
  2. 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:

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".

Mdx Query Result

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:

spinner

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.

error

It worked! It's pretty scary-looking, but at least errors will not go unnoticed.

tip

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:

  1. axes which represents the members of the hierarchies we asked for.
  2. 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.

Example data table

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. 🧑‍🎨