Building a Next.js Dashboard with Dynamic Charts and SSR
Tags: nextjs, javascript, webdev, react
Published: 2022-05-01 04:54:09


Data visualizations and analytics provide you with a graphical representation of your organization's data and can help you make data-driven decisions for your business. Building an analytics application for your organization's data is easier than you think.

In this tutorial, you will learn how to build a robust data analytics application with Next.js, Cube and Bizcharts. We'll build a Next.js dashboard like this one...

![demo_app.gif](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/e70d5986-0e76-47e1-b88e-43a9e9a0388d.gif)

...and then upgrade it to use server-side rendering (SSR):

![demo_app2.gif](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/8fa8bd57-f827-4ee5-bf00-ee77fcfe26b1.gif)

Here’s a [quick preview](https://mydashboard-ten.vercel.app/?ref=building-nextjs-dashboard-with-dynamic-charts-and-ssr) of what we are building. You can also find the complete finished code in this [GitHub repository](https://github.com/Shadid12/next-with-cube?ref=building-nextjs-dashboard-with-dynamic-charts-and-ssr).

## So, what is Cube?

Cube is an open-source, API-first [headless business intelligence](https://cube.dev/blog/headless-bi?ref=building-nextjs-dashboard-with-dynamic-charts-and-ssr) platform. Cube connects to dozens of different data sources (primarily databases) to make data accessible and consistent across every application.

You run your Cube API as a service. Cube manages connections to your databases and exposes an API for your front-end applications to consume and build data visualization and other analytics features.

![https://cubedev-blog-images.s3.us-east-2.amazonaws.com/fd2bf0fb-3bd4-4528-99a4-46ddaab774c9.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/fd2bf0fb-3bd4-4528-99a4-46ddaab774c9.png)

## Getting started with Cube

The easiest way to get started with Cube is with [Cube Cloud](http://cubecloud.dev/?ref=building-nextjs-dashboard-with-dynamic-charts-and-ssr). It provides a fully managed cube server ready to use. However, if you prefer self-hosting, then follow [this tutorial](https://cube.dev/docs/getting-started/docker/compose).

We will create a new Cube deployment in Cube Cloud. You can select a cloud platform of your choice.

![create deployment of cube](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/d4532bba-2a41-4cf8-b7de-ebdf93ff108b.png)

Next, select start from scratch to get started with a fresh instance.

![set_up_cube.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/9d474c80-8206-4b02-829d-e612501e1842.png)

Next, we will select a database. For this example application I am going to use PostgreSQL. Feel free to use any database of your choice. 

Don’t have a database with sample data? We got you covered. We created a public database for you to connect your Cube instance and play around. Add the following credentials in your Cube database configuration to connect to our public database.

```bash
Hostname:  demo-db-examples.cube.dev
Port:      5432
Database:  ecom
Username:  cube
Password:  12345
```

Cube can auto-generate a **Data Schema** from your SQL tables. A Cube.js Data Schema is used to model raw data into meaningful business definitions. The data schema is exposed through the [querying API](https://cube.dev/docs/rest-api), allowing end-users to query a wide variety of analytical queries. 

We will select the following tables for schema generation. More on Data Schema later in the article.

```sql
line_item
orders
products
product_categories
users
suppliers
```

![Generate Schema](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/913358e6-0237-48b6-984b-062d30245b6b.png)
It will take couple minutes to get up and running. 

## Visualizing data with the Developer Playground

Let’s head over to the `Playground` tab in Cube cloud. The **Developer Playground** is a tool to generate dashboards from various templates for different front-end frameworks and data visualization libraries.

We will create a new query. Let’s select `Line Item Count` as measures and `Line Item Create at` as time. We can select a framework and a data visualization library in the chart configuration. Then select Run to generate the data chart.

![playground_overview](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/12d7f912-ca18-4bb8-9299-77451197f657.png)

Once the chart is generated we can also select the edit button to view the code for the front end in code sandbox. This is extremely powerful because Cube scaffolds front-end code and gives us a template to build our front-end application. 

![bar chart example](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/2fb65e4b-0a77-4a8f-b661-9db426823c92.png)

Next, we will create a new Next.js application and create an analytics dashboard. 

## Building the Next.js App

Please run the following command to create a new Next.js app.

```bash
$ npx create-next-app myanalytics
$ cd myanalytics
```

Next, add the required npm packages to your project.

```bash
$ npm i @cubejs-client/react \
  bizcharts \
  antd \
  react-flatpickr --save
```

Create a new `.env.local` file in the root directory of your project.  Add the following environment variables.

```bash
# .env.local
NEXT_PUBLIC_CUBEJS_API_URL='<Your-Cube-API-Endpoint>'
NEXT_PUBLIC_CUBEJS_TOKEN='Your-Cube-Token'
```

 

You can find the Cube API endpoint from the Cube dashboard. Navigate to Settings from the Cube dashboard. There is a field called `Cube.js API` in the overview tab. Copy the `url` from there and add it to your `.env.local`.

![cube_api.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/b457df64-d1e0-4b5f-8542-9a05c8863bab.png)

We will also need to generate a Cube token to connect to Cube Cloud from our Next.js application. Please Select the `Env vars` tab in your Settings and copy the `CUBEJS_API_SECRET` value.

![settings.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/79458fbb-2070-45b7-b891-64f29622d732.png)

With this secret, we can generate a JWT token. You can run the following node script to generate a JWT token.

```jsx
const jwt = require('jsonwebtoken');
const CUBE_API_SECRET = '<Secret>';

const cubejsToken = jwt.sign(
	{}, CUBE_API_SECRET, { expiresIn: '30d' }
);

console.log(cubejsToken);
```

Learn more about JWT tokens and how they work on [Auth0 website](https://auth0.com/learn/json-web-tokens/). 

Copy the generated JWT token and add it to `NEXT_PUBLIC_CUBEJS_TOKEN` in `.env.local` file. We are now all set. Let’s go ahead and run our application with `npm run dev` command.

### Creating our first chart

Let's create a chart to visualize our order counts for each day over a time period. Replace the contents of `pages/index.js` with the following code.

```jsx
import { useEffect, useState } from 'react';
import cubejs from '@cubejs-client/core';
import Flatpickr from 'react-flatpickr';
import LineChart from '../components/LineChart'
import { stackedChartData } from '../util';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const cubejsApi = cubejs(
  process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
  { apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);

export default function Home() {

  const [data, setData] = useState(null);
  const [error, setError] = useState (null);
  const [dateRange, setDateRange] = useState({
    startDate: '2017-08-02',
    endDate: '2018-01-31'
  });

  useEffect(() => {
    loadData(); // function to load data from Cube
  }, [dateRange]);
  
  /**
  * This function fetches data from Cube's api
  **/
  const loadData = () => {
    cubejsApi
      .load({
        measures: ["Orders.count"],
        timeDimensions: [
          {
            dimension: "Orders.createdAt",
            granularity: `day`,
            dateRange: [dateRange.startDate, dateRange.endDate]
          }
        ]
      })
      .then((resultSet) => {
        setData(stackedChartData(resultSet));
      })
      .catch((error) => {
        setError(error);
      });
  }

  if(error) {
    return <div>Error: {error.message}</div>
  }

  if(!data) {
    return <div>Loading...</div>
  }

  return (
    <div className={styles.container}>

      <h1>Client Rendered Charts Example</h1>
      <h5>🗓️ Select a date range</h5>
      <Flatpickr
        options={{ 
          allowInput: true, 
          mode: "range", 
          minDate: new Date('2016-12-12'),
          maxDate: new Date('2020-12-12') 
        }}
        value={[dateRange.startDate, dateRange.endDate]}
        onChange={(selectedDates) => {
          if (selectedDates.length === 2) {
            setDateRange({
              startDate: selectedDates[0],
              endDate: selectedDates[1]
            })
          }
        }}
      />
      <h3>📈 Order count timeseries</h3>
      <LineChart data={data}/>

    </div>
  )
}
```

Let's review the code. First of all, we are initializing the Cube.js API client with the following lines of code.

```jsx
const cubejsApi = cubejs(
  process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
  { apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);
```

Inside the `useEffect()` hook we run a function called `loadData`. Inside the `loadData` function we call the `load` function from `cubejsApi`. This function queries Cube Cloud and returns the desired data based on the defined Cube schema.

```jsx
// ...
useEffect(() => {
    loadData();
}, [dateRange]);

const jsonQueryPlot = {
  measures: ["Orders.count"],
  timeDimensions: [
    {
      dimension: "Orders.createdAt",
      granularity: `day`,
      dateRange: [dateRange.startDate, dateRange.endDate]
    }
  ]
}
  
const loadData = () => {
  cubejsApi
    .load(jsonQueryPlot)
    .then((resultSet) => {
      setData(stackedChartData(resultSet));
    })
    .catch((error) => {
      setError(error);
    });
}
```

Notice that we pass in an object as a parameter in the `load` function. The shape of this object defines the type of data we are getting back.

We can generate this object from the Cube Playground. Let's head over to Cube Playground and execute a query. Select the `JSON Query` tab as shown in the following image. 

![json_query.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/8aa7e5ac-3e90-47df-a243-9db853826751.png)

Notice, we also import a component named `LineChart` from the `components/LineChart` file. We will pass the data as props to this component to create the chart.
Let's create a new file `components/LineChart.js` and add the following code.

```jsx
import { Chart, Axis, Tooltip, Geom } from "bizcharts"

export default function LineChart({ data }) {
  return (
    <Chart
      scale={{
        x: {
          tickCount: 8
        }
      }}
      autoFit
      height={400}
      data={data}
      forceFit
    >
      <Axis name="x" />
      <Axis name="measure" />
      <Tooltip
        crosshairs={{
          type: "y"
        }}
      />
      <Geom type="line" position="x*measure" size={2} color="color" />
    </Chart>
  )
}
```

Similarly, I will add a bar chart to visualize the order count by suppliers and a table for order count. The final version of `pages/index.js` should be as follows.

```jsx
import { useEffect, useState } from 'react';
import cubejs from "@cubejs-client/core";
import Flatpickr from "react-flatpickr";
import LineChart from '../components/LineChart'
import { stackedChartData } from '../util';
import Link from 'next/link';
import styles from '../styles/Home.module.css';
**import BarChart from '../components/BarChart';
import TableRenderer from '../components/Table';**

const cubejsApi = cubejs(
  process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
  { apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
);

export default function Home() {

  const [data, setData] = useState(null);
  const [barChartData, setBarChartData] = useState(null);
  const [error, setError] = useState (null);
  const [dateRange, setDateRange] = useState({
    startDate: '2017-08-02',
    endDate: '2018-01-31'
  });

  useEffect(() => {
    loadData();
  }, [dateRange]);

	const jsonQueryPlot = {
	  measures: ["Orders.count"],
	  timeDimensions: [
	    {
	      dimension: "Orders.createdAt",
	      granularity: `day`,
	      dateRange: [dateRange.startDate, dateRange.endDate]
	    }
	  ]
	}

   const jsonQueryBarChart = {
      measures: ["Orders.count"],
      timeDimensions: [
        {
          dimension: "Orders.createdAt",
          dateRange: [dateRange.startDate, dateRange.endDate]
        }
      ],
      order: {
        "Orders.count": "desc"
      },
      dimensions: ["Suppliers.company"],
      "filters": []
    }
  
  const loadData = () => {
    cubejsApi
      .load(jsonQueryPlot)
      .then((resultSet) => {
        setData(stackedChartData(resultSet));
      })
      .catch((error) => {
        setError(error);
      })

    **cubejsApi
      .load(jsonQueryBarChart)
      .then((resultSet) => {
        setBarChartData(stackedChartData(resultSet));
      })
      .catch((error) => {
        setError(error);
      })**
  }

  if(error) {
    return <div>Error: {error.message}</div>
  }

  if(!data || !barChartData) {
    return <div>Loading...</div>
  }

  return (
    <div className={styles.container}>
      
      <Link href={`/ssr-example?startDate=2017-08-02&endDate=2018-01-31`}>
        <a className={styles.link}>View SSR Example</a>
      </Link>

      <h1>Client Rendered Charts Example</h1>
      <h5>🗓️ Select a date range</h5>
      <Flatpickr
        options={{ 
          allowInput: true, 
          mode: "range", 
          minDate: new Date('2016-12-12'),
          maxDate: new Date('2020-12-12') 
        }}
        value={[dateRange.startDate, dateRange.endDate]}
        onChange={(selectedDates) => {
          if (selectedDates.length === 2) {
            setDateRange({
              startDate: selectedDates[0],
              endDate: selectedDates[1]
            })
          }
        }}
      />
      <h3>📈 Order count timeseries</h3>
      <LineChart data={data}/>

      **<h3>📊 Order count by Suppliers</h3>
      <BarChart 
        data={barChartData} 
        pivotConfig={{
          x: ["Suppliers.company"],
          y: ["measures"],
          fillMissingDates: true,
          joinDateRange: false
        }}
      />

      <h3>📋 Order Table</h3>
      <TableRenderer data={barChartData} />**

    </div>
  )
}
```

### Fetching dashboard data with SSR (Server Side Rendering)

Next.js provides the ability to make an API call on the server-side. You do this with `getServerSideProps` function. You can learn more about it in the [Next.js docs](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props). 

We can add the Cube API calls inside the `getServerSideProps` function and fetch all the data necessary for our dashboard on the server-side. When the page loads, the client (browser) doesn’t need to make additional API requests.

Let’s create a new page to `pages/ssr-example.js` and add the following code. 

```jsx
import cubejs from '@cubejs-client/core'
import styles from '../styles/Home.module.css'
import { stackedChartData } from '../util';
import LineChart from '../components/LineChart';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import BarChart from '../components/BarChart';
import TableRenderer from '../components/Table';
import Flatpickr from "react-flatpickr";
import { useRouter } from 'next/router';

export default function SSRCube({ data, barChartData, error }) {
  const [_, setLoading] = useState(true);
  const router = useRouter();
  const { startDate, endDate } = router.query;

  useEffect(() => {
    if (data) {
      process.nextTick(() => {
        setLoading(false);
      });
    }
  } , [data]);

  return (
    <div className={styles.container}>
      <Link href={`/`}>
        <a className={styles.link}>Client Rendered Example</a>
      </Link>
      <h1>SSR Charts Example</h1>
      
      <h5>🗓️ Select a date range</h5>

      <Flatpickr
        options={{ 
          allowInput: true, 
          mode: "range", 
          minDate: new Date('2016-12-12'),
          maxDate: new Date('2020-12-12') 
        }}
        value={[startDate, endDate]}
        onChange={(selectedDates) => {
          if (selectedDates.length === 2) {
            router.push(`/ssr-example?startDate=${selectedDates[0]}&endDate=${selectedDates[1]}`);
          }
        }}
      />

      <h3>📈 Order count timeseries</h3>
      <LineChart data={data} />

      <h3>📊 Order count by Suppliers</h3>
      <BarChart 
        data={barChartData} 
        pivotConfig={{
          x: ["Suppliers.company"],
          y: ["measures"],
          fillMissingDates: true,
          joinDateRange: false
        }}
      />

      <h3>📋 Order Table</h3>
      <TableRenderer data={barChartData} />

    </div>
  )
}

export async function getServerSideProps({ query }) {
  const cubejsApi = cubejs(
    process.env.NEXT_PUBLIC_CUBEJS_TOKEN,
    { apiUrl: process.env.NEXT_PUBLIC_CUBEJS_API_URL }
  );

  const { startDate, endDate } = query;

  try {
    const resultSet = await cubejsApi
      .load({
        measures: ["Orders.count"],
        timeDimensions: [
          {
            dimension: "Orders.createdAt",
            granularity: `day`,
            dateRange: query ? [startDate, endDate] : ['2017-08-02', '2018-01-31']
          }
        ]
      });

    const barChartResult = await cubejsApi
      .load({
        measures: ["Orders.count"],
        timeDimensions: [
          {
            dimension: "Orders.createdAt",
            dateRange: query ? [startDate, endDate] : ['2017-08-02', '2018-01-31']
          }
        ],
        order: {
          "Orders.count": "desc"
        },
        dimensions: ["Suppliers.company"],
        "filters": []
      })
    
    return {
      props: {
        data: stackedChartData(resultSet),
        barChartData: stackedChartData(barChartResult)
      }
    }
  } catch (error) {
    return {
      props: {
        error
      }
    }
  }
}
```

The key difference is that we put all the API calls inside the `getServerSideProps` function. Then we pass the data to our page component as props. 

### When is SSR useful?

Server-side rendered applications load faster on the client-side as they make all API calls on the server-side. This may not be noticeable in fast network but you can clearly notice the difference in a slow network. 

Following is a screenshot of client side rendering in a 3G network. Notice it makes 2 API calls and roughly takes about 5 seconds to load the page.

![slow-3g.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/5e5b61ee-070e-495f-8c78-28dc153ac2e6.png)

Now, compare this to the SSR version. The SSR version will make one API call as it rendered all data in the server side. It makes one call and it takes about 2 seconds. 

Next.js also caches data so the performance can be optimized more.

![ssr.png](https://cubedev-blog-images.s3.us-east-2.amazonaws.com/ccd719ee-45a4-4503-89c6-b1850405a785.png)

If you are expecting a lot of data for your dashboard and want to improve user experience regardless of the client’s network speed, then SSR is the way to go.

## Where to go from here?

In this tutorial, we build a simple metrics dashboard using Cube and Next.js. Cube comes with tons of features for data analytics and visualization. One of the best places to learn about these features is [Cube's official documentation](https://cube.dev/docs/) page.

The complete source code for this tutorial is available in [this GitHub repo](https://github.com/Shadid12/next-with-cube?ref=building-nextjs-dashboard-with-dynamic-charts-and-ssr).

Check out our tutorials and blog for more sample apps and blog posts like this one.

And join us on [Slack](https://publicslack.com/slacks/cubejs/invites/new?ref=building-nextjs-dashboard-with-dynamic-charts-and-ssr)! It is a great place to get help and stay up to date.