Smart UI Next.js SSR

Next.js Integration with Smart UI Server-Side Components

Introduction

Next.js is a powerful framework that solves problems faced with React. It is called framework because Next.js handles tooling and configuration, provides us additional structure and helps us with the optimizations for our application. You don't have to worry about routing, data fetching and integrations with third-party services because these are part of Next.js

If you want to start learning Next.js, navigate here

Next.js supports both client-side and server-side rendering. By default when you use Smart UI, you will render the components on the client-side. A great integration topic can be found here

The topic from above will show you the usage without server-side rendering. In this tutorial, we will create an application with Next.js, which renders Smart UI components server-side.

Getting started

The first thing to do is to initialize a Next.js application using the following command:

npx create-next-app smart-next-app

After running the command, a few questions will be asked.

  • Would you like to use TypeScript with this project? - No
  • Would you like to use ESLint with this project? - No
  • Would you like to use Tailwind CSS with this project? - No
  • Would you like to use `src/` directory with this project? - Yes
  • Would you like to use experimental `app/` directory with this project? - No
  • What import alias would you like configured? - Just press enter

After these steps, you will have a starter template for Next.js apps. You may see how our application looks like by running the app.

cd smart-next-app
npm run dev

Now, let's clean the excess code, open src/pages/index.js and clean the <main> tag and the unused import. Leave the <main> tag empty. Next open src/styles/Home.module.css, remove everything and paste only the following:

.main {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: center;
    padding: 6rem;
    min-height: 100vh;
    font-size: 20px;
}

Installing Smart UI

Our next task is to add Smart UI to the application. Simply run:

npm install smart-webcomponents-react

After successfully installing smart-webcomponents-react import the default CSS.

Open src/pages/_app.js and under the import of the global.css paste import smart.default.css

import 'smart-webcomponents-react/source/styles/smart.default.css';

We are ready to use any Smart UI component now.

Using Smart UI Components Client-Side Only

Let's import our first component. Open src/pages/index.js and import dynamically GanttChart.

Read more about dynamic import here.

First import dynamic:

import dynamic from 'next/dynamic'

Now, GanttChart

const GanttChart = dynamic(() => import('smart-webcomponents-react/ganttchart'), {
    ssr: false,
    loading: () => <div>Loading gantt chart...</div>
})

The ssr: false turns off the server-side rendering and loading: () => <div>Loading gantt chart...</div> is the Suspense fallback.

To have a fully functional GanttChart, replace the Home component with this one:

export default function Home() {

    const today = new Date();
    
    const ganttChartOptions = {
        durationUnit: 'hour',
        taskColumns: [{
            label: 'Employee',
            value: 'id'
        }],
        view: 'day',
        treeSize: 125,
        dataSource: [{
                id: 'Betty',
                label: 'Morning Shift',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 4),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 30),
                class: 'morning-shift',
                type: 'task'
            },
            {
                id: 'William',
                label: 'Afternoon-shift',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 30),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 20),
                class: 'afternoon-shift',
                type: 'task'
            },
            {
                id: 'Emma',
                label: 'Half-day',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 30),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 16, 30),
                class: 'half-day',
                type: 'task'
            },
            {
                id: 'Oliver',
                label: 'Morning-shift',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 4),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 30),
                class: 'morning-shift',
                type: 'task'
            },
            {
                id: 'Jason',
                label: 'Afternoon-shift',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 30),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 20),
                class: 'afternoon-shift',
                type: 'task'
            },
            {
                id: 'Alex',
                label: 'Early-morning-support',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 8, 30),
                class: 'early-morning-support',
                type: 'task'
            },
            {
                id: 'Lucas',
                label: 'Half-day',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 4, 30),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 8, 30),
                class: 'half-day',
                type: 'task'
            },
            {
                id: 'Michael',
                label: 'Early-morning-support',
                dateStart: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0),
                dateEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 8, 30),
                class: 'early-morning-support',
                type: 'task'
            }
        ]
    }
    
    return (
        <>
            <Head>
                    <title>Create Next App</title>
                    <meta name="description" content="Generated by create next app" />
                    <meta name="viewport" content="width=device-width, initial-scale=1" />
                    <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className={styles.main}>
                    <GanttChart
                        className={styles['gantt-chart']}
                        {...ganttChartOptions}
                    />
            </main>
        </>
    )
}

Open pages/styles/Home.module.css and add the following class:

.gantt-chart {
    width: 100%;
    height: auto;
}

You can see a working Gantt Chart component that renders client-side. If we want to render it server-side, change the import to this:

import { GanttChart } from 'smart-webcomponents-react/ganttchart';

Server-Side Usage

When you change the import to a regular one, you will see the following error:

Server Error
ReferenceError: window is not defined

The problem occurs because Smart Components rely on the browser APIs. We need to find a way to overcome this issue, we have to simulate these APIs server-side.

The first thing that we need to do is to create a custom server for Next.js. This seems scary, but it is not!

In the root (smart-next-app) directory, create a file called server.js

Install express with this command:

npm install express

Paste the following code to create new server for Next.js:

// server.js
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const hostname = 'localhost'
const port = 3000

// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port })
const handle = app.getRequestHandler()

app.prepare()
    .then(() => {

        const server = express()

        server.get('*', (req, res) => {
            return handle(req, res);
        })

        server.listen(port, () => {
            console.log(`Server is listening on port ${port}`);
        })
})

For more information about creating a custom web server visit Next.js' documentation here

Basically, in our server, we are passing all of the requests to Next.js and the framework handles them, as seen from the following code:

server.get('*', (req, res) => {
    return handle(req, res);
})

To run our server in dev environment, nodemon is required to enable hot reloading. You can install it with this command:

npm install nodemon

Do not forget to change the run dev script. Open the package.json and change the script as follows:

"scripts": {
    "dev": "nodemon server.js",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
}

So far so good we have created a server, but our problem is not resolved yet, as you can see if you run the application.

We need to create a window object and other browser APIs and attach them globally. We can do this task with the following fork of the domino library: @davidloiret/domino-custom-elements. We will use this form because domino does not include custom elements in it and this fork has extended domino to include them.

A link for the repository here

Install the library with the following command:

npm i @davidloiret/domino-custom-elements

After that, in server.js, before preparing the app, import domino and create a window object:

const domino = require('@davidloiret/domino-custom-elements');
const win = domino.createWindow();

app.prepare()
...

Since we have an object, we should assign it globally just before creating the server constant. We should also import Smart UI's base element and attach the Smart function globally also:

const domino = require('@davidloiret/domino-custom-elements');
const win = domino.createWindow();

app.prepare()
    .then(() => {

        global['window'] = win;
        global['document'] = win.document;
        global['navigator'] = win.navigator;
        global['HTMLElement'] = win.HTMLElement;
        global['Element'] = win.Element;
        global['DOMTokenList'] = win.DOMTokenList;
        global['Node'] = win.Node;
        global['Text'] = win.Text;

        require('smart-webcomponents-react/source/smart.element')
        global['Smart'] = window.Smart

        const server = express()
        ....

Now run the application and the Gantt chart should appear. Also, a warning will appear in the console, which is not dangerous, but it will show in production.

Conclusion

Smart UI components can be used both client-side and server-side. The client-side usage is a straight forward, but the server-side requires some configuration. That happens because the components rely on browser APIs that are not avaiable in Node environment and has to be simulated. In this tutorial, we managed to show step-by-step how to configure a Next.js application for server-side usage of Smart UI.