Real-Time Project Management with Bryntum Gantt
Tags: javascript, productivity, tutorial, webdev
Published: 2023-08-24 13:45:52

A real-time application, as the name implies, is an application that is updated in or close to real time. A good example of a real-time app is Google Docs. When it was launched in 2006, Google Docs changed the way multiple people collaborated on documents, enabling users to receive updates as they're made.

Industries such as gaming, e-commerce, and social media employ real-time web apps to facilitate seamless collaboration and improve reactivity to remote changes.

In this article, you'll learn how to build a real-time project management app using [React](https://reactjs.org), [Socket.IO](https://socket.io), and [Bryntum Gantt](https://bryntum.com/docs/gantt), a powerful tool that can help you easily set up project tasks, track their progress, and collaborate with team members.

## What Are Real-Time Web Apps?

Real-time web apps are web applications that allow for information to be passed between the server and the client in real time. They are constantly changing as soon as new data is received.

Real-time web apps use the [WebSocket protocol](https://www.rfc-editor.org/rfc/rfc6455), which provisions full-duplex (or two-way) communication between a client and a server. As opposed to a WebSocket, the HTTP used in traditional web apps has a unidirectional (or half-duplex) communication. The following image shows how a half-duplex communication protocol like HTTP and a full-duplex communication protocol like WebSocket transfer data between a server and a client:

![Half-duplex vs. full-duplex courtesy of Gideon Idoko](https://i.imgur.com/wO9OV5o.png)

WebSockets make it possible for the client and server to send and receive data in real time over a single transmission control protocol (TCP) socket connection. A WebSocket connection is persistent and is kept open until either the server or the client terminates it. In contrast, an HTTP connection is temporary, as it's closed as soon as the response from the server is received. If you need to make a new request, a fresh connection has to be created.

Real-time web apps have several advantages over traditional web apps. They are more responsive, reliable, scalable, and able to handle more concurrent users who need to receive updates in real time.

## Building a Real-Time Project Management App

Now that you know what real-time web apps and WebSockets are, it's time to learn how to leverage them with React, Socket.IO, and Bryntum Gantt to create a simple but real-time project task management tool. The application will work like this at the end of this article:

[Click to view gif](https://i.ritzastatic.com/images/3218f8f2ef7b48a3917aa6e6dc5fdd6c/Final_app.mp4)


You can find the full code for this tutorial on this [GitHub repo](https://github.com/IamGideonIdoko/realtime-proman).

### Prerequisites

Before you begin, you'll need to have [Node.js](​​https://nodejs.org/en/) installed, as it's needed to run React in development mode on your local machine. Node.js also ships with the [Node Package Manager (npm)](https://www.npmjs.com), which enables the installation of some required packages, like Bryntum Gantt. If you haven't already, download the [LTS version of Node.js](https://nodejs.org) and install it.

Bryntum Gantt is the most reliable, easy-to-use, and full-featured JavaScript Gantt chart component. It's compatible with all major browsers and offers a wide range of features that can be fully customized to suit your specific needs. Bryntum Gantt's rendering engine is fast, and it can handle large data sets without halting performance. In addition, its built-in scheduling engine provides asynchronous rescheduling of any number of tasks.

Bryntum Gantt will provide the Gantt chart and other necessary components, like buttons, that will be configured to meet the needs of the application. Now it's time to build it.

### Basic Setup

The app will be bootstrapped with [Create React App (CRA)](https://createreactapp.dev). Thankfully, Bryntum has a CRA template. This will enable users to quickly get started with the Bryntum Gantt package. Bryntum packages are commercial, but they have trial versions that will be sufficient for this tutorial.

The commercial and trial versions are available in Bryntum's private registry. Open your terminal and run the following command to locate the registry:

```bash
npm config set "@bryntum:registry=https://npm.bryntum.com"
```

Then log in:

```bash
npm login --registry=https://npm.bryntum.com
```

You will be required to enter your username, password, and email address. Use your email with the "@" replaced with ".." as your username and "trial" as your password:

```bash
Username: user..yourdomain.com
Password: trial
Email: (this IS public) user@yourdomain.com
```

This will give you access to Bryntum's private registry and packages.

Now that you have access to the registry, it's time to finish up your setup. To do so, create a new directory for the project:

```bash
mkdir realtime-proman
```

You can replace `realtime-proman` with your preferred app name.

Then bootstrap a new React client with the Bryntum Gantt template:

```bash
npx create-react-app client --template @bryntum/cra-template-javascript-gantt
```

Since a WebSocket will be used, a simple server is required. Create a new directory for the server-side code in the root directory of your project:

```bash
mkdir server
```

Change to the server directory and initialize a new `npm` project:

```bash
cd server && npm init -y
```

Finally, create an `index.js` file that will contain the server-side code:

```bash
touch index.js
```

### Creating a WebSocket Server

Now you need to create a simple WebSocket server that your React client can interact with. In the `server` directory, run the following command to install the server-required dependency, Socket.IO:

```bash
npm install socket.io
```

Then in the `index.js` file you created earlier, paste the following code:

```jsx
const 
    { Server } = require('socket.io'),
    io = new Server({
        cors: { origin: '*' }
    });
// data will be persisted here
let persistedData = null;
io.on('connection', (socket) => {
    console.log(`New connection ID: ${socket.id}`);
    socket.emit('just-joined', persistedData ?? {});
    socket.on('data-change', (data) => {
        persistedData = data;
        socket.broadcast.emit('new-data-change', data);
    })
});
io.listen(5000);
console.log(`🚀 socket server started on port 5000...`);
```

Here, you have three events: `data-change`, `new-data-change`, and `just-joined`. When the `data-change` event is fired, the passed data is persisted to `persistData`, and the `new-data-change` event is emitted with that data. The `just-joined` event is emitted with the persisted data on every new connection.

Finally, start the WebSocket server:

```jsx
node index
```

### Working on the Client

Now that the WebSocket server is up and running, it's time to finish up on the client side. A peek in the `client` directory will reveal its structure, which looks like this:

```
client                                                          
├─ public                                                                        
│  ├─ data                                                                       
│  │  └─ gantt-data.json                                                         
│  ├─ favicon.png                                                                
│  ├─ index.html                                                                 
│  └─ manifest.json                                                              
├─ src                                                                           
│  ├─ App.js                                                                     
│  ├─ App.scss                                                                   
│  ├─ GanttConfig.js                                                             
│  └─ index.js                                                                   
├─ package-lock.json                                                             
├─ package.json                                                                  
└─ README.md
```

The `gantt-data.json` file holds some dummy data that is loaded once the `App` component renders. Then the `GanttConfig.js` file holds the configuration for your Gantt chart.

Create a new terminal window in the `client` directory and spin up your React development server:

```jsx
npm start
```

When the React development server is up, you should see something like the following:

![Initial app](https://i.imgur.com/RefjJgy.png)

The data you see here is from the `gantt-data.json` file and is bound to the `BryntumGantt` via [the project transport method](https://www.bryntum.com/docs/gantt/guide/Gantt/integration/react/data-binding#using-project-transport) when the app first loads. This is defined in the Gantt chart configuration in the `GanttConfig.js` file like so:

```jsx
// ...
project : {
        transport : {
            load : {
                url : 'data/gantt-data.json'
            }
        },
        autoLoad  : true,
    }
// ...
```

The path to the `gantt-data.json` file is passed as the value for the `url` option. A `GET` request is made to the path to fetch its JSON data when the `App` component initially renders. This is why the content of the `gantt-data.json` file resembles a server response. It even has a `success` key with `true` value. Aside from the `success` key, you'll find seven root keys:

1. **`project`:** holds the configuration for your [ProjectModel](https://www.bryntum.com/docs/gantt/api/Gantt/model/ProjectModel), which is a central store for all the data in your project.

> **Please note:** A [model](https://www.bryntum.com/docs/gantt/api/Core/data/Model) is simply the definition of a record that can be added to (or loaded into) a [store](https://www.bryntum.com/docs/gantt/api/Core/data/Store). A store is a data container that holds flat data or tree structures. A record is a data container and an item in a store.

2. **`calendars`:** holds all calendars passed to the [CalendarModel](https://www.bryntum.com/docs/gantt/api/Gantt/model/CalendarModel).

3. **`tasks`:** holds all the tasks passed to the [TaskModel](https://www.bryntum.com/docs/gantt/api/Gantt/model/TaskModel).

4. **`dependencies`:** holds all the single dependencies between tasks. It's passed to the [DependencyModel](https://www.bryntum.com/docs/gantt/api/Gantt/model/TaskModel).

5. **`resources`:** holds all resources or assets passed to the [ResourceModel](https://www.bryntum.com/docs/gantt/api/Gantt/model/ResourceModel).

6. **`assignments`:** holds all single assignments of a resource to a task passed to the [AssignmentModel](https://www.bryntum.com/docs/gantt/api/Gantt/model/AssignmentModel).

7. **`timeRanges`:** holds all ranges of time in the timeRangeStore of the ProjectModel. Each range represents a [TimeSpan](https://www.bryntum.com/docs/gantt/api/Scheduler/model/TimeSpan) model.

Now, it's time to make use of the default JSON data loaded into the Gantt chart.

#### Adding a Toolbar

To begin, you need to add a toolbar to the Gantt chart that will have two button components for creating and editing tasks, respectively.

Create a `GanttToolbar.js` file in the `src` folder of your React client and add the following code to it:

```jsx
import {
    Toolbar,
    Toast,
} from '@bryntum/gantt';

export default class GanttToolbar extends Toolbar {
    // Factoryable type name
    static get type() {
        return 'gantttoolbar';
    }

    static get $name() {
        return 'GanttToolbar';
    }

    // Called when toolbar is added to the Gantt panel
    set parent(parent) {
        super.parent = parent;

        const me = this;

        me.gantt = parent;

        me.styleNode = document.createElement('style');
        document.head.appendChild(me.styleNode);
    }

    get parent() {
        return super.parent;
    }

    static get configurable() {
        return {
            items: [
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            color    : 'b-green',
                            ref      : 'addTaskButton',
                            icon     : 'b-fa b-fa-plus',
                            text     : 'Create',
                            tooltip  : 'Create new task',
                            onAction : 'up.onAddTaskClick'
                        }
                    ]
                },
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            ref      : 'editTaskButton',
                            icon     : 'b-fa b-fa-pen',
                            text     : 'Edit',
                            tooltip  : 'Edit selected task',
                            onAction : 'up.onEditTaskClick'
                        }
                    ]
                },
            ]
        };
    }

    async onAddTaskClick() {
        const { gantt } = this,
              added     = gantt.taskStore.rootNode.appendChild({
                name     : 'New task',
                duration : 1
            });

        // wait for immediate commit to calculate new task fields
        await gantt.project.commitAsync();

        // scroll to the added task
        await gantt.scrollRowIntoView(added);

        gantt.features.cellEdit.startEditing({
            record : added,
            field  : 'name'
        });
    }

    onEditTaskClick() {
        const { gantt } = this;

        if (gantt.selectedRecord) {
            gantt.editTask(gantt.selectedRecord);
        } else {
            Toast.show('First select the task you want to edit');
        }
    }
}

// Register this widget type with its Factory
GanttToolbar.initClass();
```

This registers a container widget with two buttons in it.

Update your `GanttConfig.js` with the following:

```jsx
import './GanttToolbar';
/**
 * Application configuration
 */

const ganttConfig = {
    columns    : [
        { type : 'name', field : 'name', width : 250 }
    ],
    viewPreset : 'weekAndDayLetter',
    barMargin  : 10,

    project : {
        transport : {
            load : {
                url : 'data/gantt-data.json'
            }
        },
        autoLoad  : true,
    },
    tbar : { type: 'gantttoolbar' }, // toolbar
};

export { ganttConfig };
```

This imports the `GanttToolbar.js` file and adds the registered toolbar type to the `tbar` option of the Gantt chart configuration.

Save all the files and reload your browser to view the new toolbar:

![App with toolbar](https://i.imgur.com/rsF5gat.png)

Also, you should be able to create a new task or edit existing ones by clicking on the **CREATE** or **EDIT** buttons, respectively.

At this point, you've created a project task management tool, but it's not in real time yet. Your React client has to exchange data with the WebSocket server for it to be real time.

#### Implementing WebSocket on the Client

To implement WebSocket on the client, start by installing the Socket.IO client library, along with a universally unique identifier (UUID) package that will help to identify clients in a socket connection.

Open a terminal in the `client` directory and run the following command:

```bash
npm install socket.io-client uuid
```

Once the installation is complete, import the `io` and `v4` methods from the installed packages and the `useEffect` and `useRef` hooks from `react` in the `App.js` file:

```jsx
import { useRef, useEffect } from 'react';
import { io } from 'socket.io-client';
import { v4 as uuid } from 'uuid';
```

Initialize the socket client just before the `App` component definition:

```jsx
const socket = io('ws://localhost:5000', { transports: ['websocket'] });
```

Update the `App` component with the following code:

```jsx
function App() {
    const 
        ganttRef = useRef(null), // reference to the gantt chart
        remote = useRef(false), // set to true when changes are made to the store
        clientId = useRef(uuid()), // a unique ID for the client
        persistedData = useRef(), // store persisted data from the server
        canSendData = useRef(false); // set to true to allow data to be sent to the server

    useEffect(() => {
        const gantt = ganttRef.current?.instance;
        if (gantt) {
            // access the ProjectModel
            const project = gantt.project;
            // listen for change events
            project?.addListener('change', () => {
                // don't send data when the same client is updating the project store
                if (!remote.current) {
                    // only send data when allowed
                    if (canSendData.current) {
                        // get project store
                        const store = gantt.store.toJSON();
                        // send data to the store with the data-change event
                        socket.emit('data-change', { senderId: clientId.current, store });
                    }
                }
            });
            // trigger when project data is loaded
            project?.addListener('load', () => {
                if (persistedData.current) {
                    const { store } = persistedData.current;
                    remote.current = true;
                    if (store) {
                        // update project store if persisted store exists
                        gantt.store.data = store
                    }
                    remote.current = false;
                    // allow data to be sent 2s after data is loaded
                    setTimeout(() => canSendData.current = true, 2000);
                    // stop listening since this data from this event is needed once
                    socket.off('just-joined');
                }
            });
            socket.on('just-joined', (data) => {
                // update with the persisted data from the server
                if (data) persistedData.current = data;
            });

            socket.on('new-data-change', ({ senderId, store }) => {
                // should not update store if received data was sent by same client
                if (clientId.current !== senderId) {
                    // disable sending sending data to server as change to the store is to be made
                    remote.current = true;
                    // don't update store if previous data was sent by same client
                    if (JSON.stringify(gantt.store.toJSON()) !== JSON.stringify(store)) {
                        // update store with store from server
                        gantt.store.data = store;
                    }
                    // data can now be sent to the server again
                    remote.current = false;
                }
            });
        }
        return () => {
            // disconnect socket when this component unmounts
            socket.disconnect();
        };
    }, []);

    return (
        <BryntumGantt
            ref = {ganttRef}
            {...ganttConfig}
        />
    );
}
```

The comments on the code explain what each line does, but the state of the project store is sent and received via the WebSocket. The project store is updated upon receiving a new state from the WebSocket server.

Save and reload your app in the browser. Then open your app in another browser window and create or edit tasks in the app in one of the windows. You should see the changes reflected in real time in the app in the other window as follows:

[Click to view gif](https://i.ritzastatic.com/images/3218f8f2ef7b48a3917aa6e6dc5fdd6c/Final_app.mp4)

## Conclusion

Real-time web apps offer an engaging user experience that lets multiple individuals interact with the app in real time. In this article, you were introduced to what real-time web apps are and what makes them real time. You also learned how to use the [Bryntum Gantt chart](https://bryntum.com/docs/gantt), React, and a WebSocket to build a simple real-time project task management app. In the process, you learned how to create a WebSocket server and how to communicate with it from a React client.

[Bryntum](https://www.bryntum.com) is an industry leader when it comes to world-class web components. They provide powerful and mature components, like Bryntum Gantt, which was used here. Other web components from Bryntum include the [Bryntum Scheduler](https://www.bryntum.com/docs/scheduler/), [Bryntum Grid](https://www.bryntum.com/docs/grid/), [Bryntum Calendar](https://www.bryntum.com/products/calendar/), [Bryntum Task Board](https://www.bryntum.com/docs/taskboard), and even testing tools like [Siesta](https://www.bryntum.com/products/siesta/). These components are highly customizable, easily integrated, and you can [try them for free today](https://www.bryntum.com)!

Are you already using a Bryntum web component? Let us know what you think!