This tutorial describes how to build a React Gantt chart that synchronizes task and link data across multiple clients in real time using Firebase Firestore. This functionality is especially useful for:
You'll learn how to:
You can check the corresponding example on GitHub: DHTMLX React Gantt with Firebase Firestore Demo.
Start by creating a React + Vite project. Install the required dependencies as follows:
npm install @dhx/trial-react-gantt firebase
First, create a Firebase project by implementing the following steps:
react-gantt-firebase) and follow the setup promptsThen set up Firestore by completing the steps below:
After that, register your web app in the following way:
</> to register a new web appreact-gantt-firebase)Finally, configure Firebase in your project as described below:
.env file in the following way:VITE_FIREBASE_CONFIGURATION = {
"apiKey": "YOUR_API_KEY",
"authDomain":"react-gantt-firebase.firebaseapp.com",
"projectId": "react-gantt-firebase",
"storageBucket": "react-gantt-firebase.firebasestorage.app",
"messagingSenderId": "693536970600",
"appId": "1:693536970600:web:1b3fa4e4b032acaab368dd"
}
Replace the YOUR_API_KEY placeholder with your actual Firebase project credentials.
firebase.ts
import { initializeApp } from "firebase/app";
import { getFirestore, collection, query } from "firebase/firestore";
const firebaseConfig = JSON.parse(import.meta.env.VITE_FIREBASE_CONFIGURATION);
initializeApp(firebaseConfig);
const db = getFirestore();
const tasksCollection = collection(db, "tasks");
const linksCollection = collection(db, "links");
const tasksQuery = query(tasksCollection);
const linksQuery = query(linksCollection);
export { db, tasksQuery, linksQuery, tasksCollection, linksCollection };
To begin with, set up the core Gantt component with React state for tasks and links with the following configuration:
const [tasks, setTasks] = useState<Task[]>([]);
const [links, setLinks] = useState<Link[]>([]);
const templates: GanttTemplates = {
parse_date: (date) => new Date(date),
format_date: (dateString) => dateString.toISOString(),
};
const config: GanttConfig = {
auto_scheduling: true,
};
In the snippet above:
Templates allow us to control how dates are parsed and formatted inside the Gantt component.
Since Firestore stores dates as strings, we need to convert them back to Date objects (parse_date) and correctly format them for storage (format_date).
The auto_scheduling option enables automatic recalculation of dependent tasks when a parent task is moved or changed.
This is useful for project management scenarios.
Now, create state handlers to manage the Gantt's internal state as in:
const createStateHandlers = <T extends { id: string | number }>(
setState: React.Dispatch<React.SetStateAction<T[]>>
): EntityHandler<T> => ({
added: (item) => setState((prev) => (prev.find((i) => i.id ===
item.id) ? prev : [...prev, item])),
modified: (item) => setState((prev) => prev.map((i) => (i.id ===
item.id ? { ...i, ...item } : i))),
removed: (item) => setState((prev) => prev.filter((i) => i.id !==
item.id)),
});
const taskHandlers = createStateHandlers<Task>(setTasks);
const linkHandlers = createStateHandlers<Link>(setLinks);
This provides a unified way to update local state when Firebase data changes.
Since, we are working with two types of entities - tasks and links, we can create a unified configuration object to handle both instead of duplicating the code. This object maps each entity type to its Firestore collection, API path, and state handlers. Check the code snippet below:
const entityConfig = {
task: {
collection: tasksCollection,
path: "tasks",
handlers: taskHandlers,
},
link: {
collection: linksCollection,
path: "links",
handlers: linkHandlers,
},
};
You can check the overview of the resulting project structure in the following scheme:
src/
├── App.tsx # Entry point
├── App.css # Styles
├── components/
│ └── Gantt/
│ ├── Gantt.tsx # Main logic
│ └── types.ts # Type declarations
├── config/
│ └── firebase.ts # Firebase setup
└── main.tsx # React root
When the component mounts, you should load all tasks and links like this:
useEffect(() => {
let unsubscribeTasks: () => void;
let unsubscribeLinks: () => void;
(async () => {
const tasksSnap = await getDocs(tasksQuery);
const bulkTasks = tasksSnap.docs.map(processEntity) as Task[];
const linksSnap = await getDocs(linksQuery);
const bulkLinks = linksSnap.docs.map(processEntity) as Link[];
setTasks(bulkTasks);
setLinks(bulkLinks);
const unsubscribers = watchRealtime();
unsubscribeTasks = unsubscribers.unsubscribeTasks;
unsubscribeLinks = unsubscribers.unsubscribeLinks;
})();
return () => {
if (unsubscribeTasks) unsubscribeTasks();
if (unsubscribeLinks) unsubscribeLinks();
};
}, []);
To convert Firebase documents to Gantt-compatible objects, use processEntity as provided below:
const processEntity = (docSnapshot: QueryDocumentSnapshot): Task | Link => {
return { ...docSnapshot.data(), id: docSnapshot.id };
};
Use Firebase's onSnapshot to subscribe to changes in both collections and unsubscribe when the component is unmounted:
function watchRealtime() {
let tasksLoaded = false;
let linksLoaded = false;
const unsubscribeTasks = onSnapshot(tasksQuery, (querySnapshot) => {
if (!tasksLoaded) {
tasksLoaded = true;
return;
}
handleRealtimeUpdates(querySnapshot, "task");
});
const unsubscribeLinks = onSnapshot(linksQuery, (querySnapshot) => {
if (!linksLoaded) {
linksLoaded = true;
return;
}
handleRealtimeUpdates(querySnapshot, "link");
});
return { unsubscribeTasks, unsubscribeLinks };
}
The first onSnapshot call returns the initial data, not the changes, that's why in watchRealtime we ignore the first call (since we've already loaded the initial data).
You can process the real-time updates using the function specified in the following code sample:
function handleRealtimeUpdates(querySnapshot: QuerySnapshot, type: GanttEntityType) {
const config = entityConfig[type];
if (!config) throw new Error(`Unknown entity type: ${type}`);
const { handlers } = config;
querySnapshot.docChanges().forEach((change) => {
if (change.doc.metadata.hasPendingWrites) return;
const handler = handlers[change.type];
if (!handler) {
throw new Error(`Unknown change type: ${change.type}`);
}
const data = processEntity(change.doc);
(handler as (data: Task | Link) => void)(data);
});
}
This method ensures that only the server-confirmed changes are processed, avoiding local duplication.
docChanges() returns the list of changes (added, modified, removed) that have been made in the Firestore collection since the last snapshot. Firestore provides the type of change (added, modified, removed), and we route it to the
corresponding handler to update the React state.
To handle the create, update, and delete requests from the Gantt component, use the logic of the data.save method which is given below:
const data = {
save: async (
entity: GanttEntityType,
action: GanttActionType,
raw: any, id: string | number
) => {
try {
const config = entityConfig[entity];
if (!config) throw new Error(`Unknown entity type: ${entity}`);
const { collection, path, handlers } = config;
const ref = doc(db, path, id.toString());
switch (action) {
case "create": {
const addedDoc = await addDoc(collection, raw);
handlers.added({ ...raw, id: addedDoc.id });
break;
}
case "update": {
await updateDoc(ref, raw);
handlers.modified(raw);
break;
}
case "delete": {
await deleteDoc(ref);
handlers.removed(raw);
break;
}
default:
throw new Error(`Unknown action type: ${action}`);
}
} catch (err) {
console.error(`Failed to ${action} ${entity}:`, err);
}
},
};
Firebase will automatically propagate these changes to all connected clients via the snapshot listeners.
Then, render the Gantt Chart with the following code:
return (
<div style=height: "100%", display: "flex", flexDirection: "column">
<ReactGantt
tasks={tasks}
links={links}
templates={templates}
config={config}
data={data}
/>
</div>
);
The data prop connects Gantt's built-in editing to the Firebase save logic provided above.
Once your project is fully working and real-time synchronization is functioning correctly, you can deploy it to make it publicly accessible on the web. There are two ways to deploy the project you can choose from: via the Firebase CLI and via the Firebase console.
This is the most efficient method, especially if you plan to update your project regularly. Follow the steps below:
1. First, if you haven't installed the Firebase CLI yet, install it using the following command:
npm install -g firebase-tools
2. Then, log in to Firebase by using the command given below:
firebase login
3. After that, initialize Firebase in your project with the following command:
firebase init
During the initialization complete the steps provided below:
dist or build, depending on your vite.config.ts or package.json setup)index.html4. Now, build the project using this code line:
npm run build
It will generate the production-ready files in the dist (or build) folder.
5. Finally, you can deploy to Firebase by running the following code line:
firebase deploy
After deployment has finished, Firebase will provide you with a link to your hosted project.
If you prefer to quickly publish the app without using the CLI, you can do it directly through the Firebase Console. These are the steps you need to complete:
1. Build the project by running the code line below:
npm run build
2. Go to Firebase Hosting → Your Project → Hosting
3. Click "Get Started" or "Upload"
4. Upload the contents of the dist (or build) folder
5. Confirm the upload and Firebase will provide you with a public URL for your site
In this tutorial we've built a real-time Gantt chart with Firebase synchronization. You've learned how to:
This approach is perfect for collaborative project management tools, where all users need to see live updates without refreshing the page.
Back to top