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, Socket.IO, and Bryntum 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, 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:
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:
You can find the full code for this tutorial on this GitHub repo.
Prerequisites
Before you begin, you'll need to have Node.js 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), which enables the installation of some required packages, like Bryntum Gantt. If you haven't already, download the LTS version of Node.js 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). 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:
npm config set "@bryntum:registry=https://npm.bryntum.com"
Then log in:
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:
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:
mkdir realtime-proman
You can replace realtime-proman
with your preferred app name.
Then bootstrap a new React client with the Bryntum Gantt template:
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:
mkdir server
Change to the server directory and initialize a new npm
project:
cd server && npm init -y
Finally, create an index.js
file that will contain the server-side code:
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:
npm install socket.io
Then in the index.js
file you created earlier, paste the following code:
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:
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:
npm start
When the React development server is up, you should see something like the following:
The data you see here is from the gantt-data.json
file and is bound to the BryntumGantt
via the project transport method when the app first loads. This is defined in the Gantt chart configuration in the GanttConfig.js
file like so:
// ...
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:
-
project
: holds the configuration for your ProjectModel, which is a central store for all the data in your project.
Please note: A model is simply the definition of a record that can be added to (or loaded into) a 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.
calendars
: holds all calendars passed to the CalendarModel.tasks
: holds all the tasks passed to the TaskModel.dependencies
: holds all the single dependencies between tasks. It's passed to the DependencyModel.resources
: holds all resources or assets passed to the ResourceModel.assignments
: holds all single assignments of a resource to a task passed to the AssignmentModel.timeRanges
: holds all ranges of time in the timeRangeStore of the ProjectModel. Each range represents a 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:
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:
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:
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:
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:
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:
const socket = io('ws://localhost:5000', { transports: ['websocket'] });
Update the App
component with the following code:
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:
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, 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 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, Bryntum Grid, Bryntum Calendar, Bryntum Task Board, and even testing tools like Siesta. These components are highly customizable, easily integrated, and you can try them for free today!
Are you already using a Bryntum web component? Let us know what you think!
Latest comments (0)