by King AJ
In today's rapidly evolving digital environment, real-time collaboration has become a paramount feature in various web applications. The ability to work together seamlessly, irrespective of geographical boundaries, has transformed how teams collaborate and communicate. This article will show how to build a web app providing a real time collaboration board using React and Node.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay โ an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
Real-time collaboration involves multiple users interacting dynamically and instantly while collaborating on a shared task or project. Unlike older collaboration methods with communication delays and updates, real-time collaboration lets team members contribute, edit, and see changes in the moment. This instant syncing promotes a feeling of togetherness and speeds up decision-making, making it a vital tool for contemporary workflows. Incorporating real-time collaboration features brings a host of benefits:
Global Reach: Real-time collaboration allows remote teams to collaborate as if they were in the same physical location. This feature is particularly valuable in an era where remote work has become the norm, enabling teams spread across the globe to collaborate seamlessly.
Brainstorm Together: Real-time collaboration sparks group brainstorming and idea-sharing, letting everyone collectively tackle challenges and create fresh solutions.
Efficiency Boost: Real-time collaboration eliminates communication delays, letting team members engage and contribute on the spot. This speeds up solving problems and streamlines workflows. There are different forms of real-time collaboration, such as: Document Collaboration, Video Conferencing, Instant Messaging and Chat, Project Management Tools, Co-browsing, Shared Calendars, Interactive Presentations, Social Media Collaboration, and Multiplayer Gaming. For this article, we'll be focusing on Real-time Whiteboarding. A Real-time whiteboard is a dynamic digital tool replicating a traditional whiteboard's functionality in an online environment. It provides individuals and teams with a shared canvas where they can collaborate in real-time, allowing for the creation, manipulation, and visualization of ideas, concepts, and information.
Our project
Using React
and Node.js
, we will delve into the exciting realm of real-time collaboration by building a real-time collaborating board using React
and Node.js
. Our project will enable users to work on a shared virtual board in real time, instantly updating the content and changes for all participants. We will incorporate the drag-and-drop feature so users can effortlessly move and arrange elements on the board, making collaboration even more intuitive and engaging. Whether you want to build a collaborative tool for remote teams, an educational platform, a project management application, or any other project demanding real-time collaboration, this article will equip you with the essential skills and knowledge to develop interactive and efficient real-time applications. So, letโs dive in and unlock the immense potential of real-time collaboration with React
and Node.js.
To set up our React app for this project, we'll do the following:
- Create React Application: Navigate to your desired directory, open your terminal, and run the following command to create a new React application using
create-react-app
:
npx create-react-app collaborate_client
- Navigating into the Project Directory: To move into the newly created project directory;
cd collaborate_client
Installing Dependencies
With our project initialized, it's time to install the necessary dependencies that will power our real-time collaborating board. These dependencies include socket.io
for real-time communication and RoughJS
for drawing capabilities.
socket.io
: Install thesocket.io
library to establish WebSocket connections for real-time data exchange;
npm install `socket.io`
- RoughJS: Integrate the rough.js library to enable drawing functionality on the collaborating board;
npm install --save roughjs
Creating the Collaborating Board User Interface with React
The' Canvas' component lies the heart of our real-time collaborating board. The Canvas
is an HTML element that serves as a blank slate where we can paint, draw, and manipulate graphical elements using JavaScript. For the user interface, we'll create a WhiteBoard
component where users can manipulate graphical elements in our React
application.
Building a Canvas Component using React
Before delving into RoughJS
and drawing capabilities, let's set the stage by creating our WhiteBoard
component. In your React
project, navigate to the appropriate directory and create a new file named Whiteboard.js. Inside this file, define your WhiteBoard
component:
import React, { useLayoutEffect } from "react";
const WhiteBoard = () => {
useLayoutEffect(() => {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// Set canvas dimensions and initial styles
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.strokeStyle = "black";
ctx.lineWidth = 5;
// Implement drawing functionality here
}, []);
//implement event listeners for drawing interactions
return (
<>
<canvas
id="canvas"
width={window.innerWidth}
height={window.innerHeight}
></canvas>
</>
);
};
export default WhiteBoard;
In the above code, we import the necessary dependencies, create a WhiteBoard
functional component, and utilize the useLayoutEffect
hooks provided by React
. Within the useLayoutEffect
hook, we access the canvas
element and its 2D rendering context to configure its dimensions and initial styles.
Integrating RoughJS within the Canvas Component
RoughJS
is a lightweight library that enables us to create hand-drawn, sketch-like graphics within our canvas. By integrating RoughJS
, we can transform our plain whiteboard into a playground of creativity, where lines, shapes, and textures come to life with an organic, handcrafted feel. To augment our WhiteBoard
component with the RoughJS
within the component file, update the code as follows:
import React, { useLayoutEffect } from "react";
import rough from "roughjs";
const WhiteBoard = () => {
useLayoutEffect(() => {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// Initialize RoughJS instance
const roughCanvas = rough.canvas(canvas);
// Set canvas initial styles
ctx.strokeStyle = "black";
ctx.lineWidth = 5;
// Add event listeners for drawing interactions
// Implement drawing functionality here
}, []);
return (
<>
<canvas
id="canvas"
width={window.innerWidth}
height={window.innerHeight}
></canvas>
</>
);
};
export default WhiteBoard;
In the updated code, we've imported the RoughJS
library and created an instance using the rough.canvas()
method, associating it with our canvas
element. This instance, stored in the roughCanvas
, will allow us to apply RoughJS
primitives and effects with which we can draw on the whiteboard. With RoughJS,
there are endless possibilities for shapes, lines, and shades we can draw. For this article, we'll look at how to draw lines and rectangles on our Whiteboard. You can build on this knowledge and add other shapes and features RoughJS supports.
Drawing lines on our Canvas
To draw lines on our canvas
using RoughJS
we'll do the following;
- Initialize Drawing State: Begin by setting up the necessary state to track drawing interactions:
const [drawing, setDrawing] = useState(false);
const [elements, setElements] = useState([]);
- Handle Mouse Down Event: When a user initiates drawing by pressing the mouse button, we'll set the
drawing
state to true and create a new element. In thehandleMouseDown
function, we're utilizing the initialclientX
andclientY
values to mark the starting point of our drawing. When the user clicks the mouse, we want to record where that click occurred, as it will be the start of the line they're about to draw:
const handleMouseDown = (e) => {
setDrawing(true);
const { clientX, clientY } = e;
// Create a new line element with the same start and end points
const element = createElement(clientX, clientY, clientX, clientY);
setElements((prevState) => [...prevState, element]);
};
- Handle Mouse Move Event: With the mouse button still down, we continuously update the element we created in
handleMouseDown
with the current path of the mouse as the user moves the mouse on thecanvas
:
const handleMouseMove = (e) => {
if (!drawing) return;
const { clientX, clientY } = e;
const index = elements.length - 1;
const { x1, y1 } = elements[index];
// Update the end point of the element and create an element
const updatedElement = createElement(x1, y1, clientX, clientY);
const elementsCopy = [...elements];
elementsCopy[index] = updatedElement;
setElements(elementsCopy);
};
- Creating our line: The mouse coordinates will be sent to the
createElement
function, which then utilizes theRoughJS
library to generate a hand-drawn representation of the element. This function then returns the coordinates and theRoughJS
element, which will be stored in ourelements
state:
const createElement = (x1, y1, x2, y2) => {
// Use the RoughJS generator to create a rough element (line or rectangle)
const roughElement = generator.line(x1, y1, x2, y2);
// Return an object representing the element, including its coordinates and RoughJS representation
return { x1, y1, x2, y2, roughElement };
};
- Rendering our elements: Using our
useLayoutEffect
function with every update to theelements
state we render the elements stored in thestate
.
useLayoutEffect(() => {
// Get the canvas element by its ID
const canvas = document.getElementById("canvas");
// Get the 2D rendering context of the canvas
const ctx = canvas.getContext("2d");
// Create a RoughJS canvas instance associated with the canvas element
const roughCanvas = rough.canvas(canvas);
// Set stroke style and line width for the canvas context
ctx.strokeStyle = "black";
ctx.lineWidth = 5;
// Clear the entire canvas to ensure a clean drawing surface
ctx.clearRect(0, 0, canvas.width, canvas.height);
// If there are saved elements to render
if (elements && elements.length > 0) {
// Iterate through each saved element
elements.forEach(({ roughElement }) => {
// Use RoughJS to draw the element on the canvas
roughCanvas.draw(roughElement);
});
}
}, [elements]); // This effect depends on the 'elements' state; re-run when it changes
- Handle Mouse Up Event: When the user releases the mouse button, we set the
drawing
state to false, halting the drawing process;
const handleMouseUp = (e) => {
setDrawing(false);
};
By implementing these steps, users can draw lines by clicking and dragging the mouse cursor on the canvas
. Here's the WhiteBoard
component with the ability to draw lines on our canvas
;
import React, { useState, useLayoutEffect } from "react";
import rough from "roughjs/bundled/rough.esm.js";
// Create a RoughJS generator instance
const generator = rough.generator();
const WhiteBoard = () => {
// State for managing drawing elements and interactions
const [elements, setElements] = useState([]);
const [drawing, setDrawing] = useState(false);
// UseLayoutEffect: Responsible for rendering drawing elements
useLayoutEffect(() => {
// Get the canvas element by its ID
const canvas = document.getElementById("canvas");
// Get the 2D rendering context of the canvas
const ctx = canvas.getContext("2d");
// Create a RoughJS canvas instance associated with the canvas element
const roughCanvas = rough.canvas(canvas);
// Set stroke style and line width for the canvas context
ctx.strokeStyle = "black";
ctx.lineWidth = 5;
// Clear the entire canvas to ensure a clean drawing surface
ctx.clearRect(0, 0, canvas.width, canvas.height);
// If there are saved elements to render
if (elements && elements.length > 0) {
// Iterate through each saved element
elements.forEach(({ roughElement }) => {
// Use RoughJS to draw the element on the canvas
roughCanvas.draw(roughElement);
});
}
}, [elements]);
// Function to create a new drawing element
const createElement = (x1, y1, x2, y2) => {
// Use the RoughJS generator to create a rough element (line or rectangle)
const roughElement = generator.line(x1, y1, x2, y2);
// Return an object representing the element, including its coordinates and RoughJS representation
return { x1, y1, x2, y2, roughElement };
};
// Event handler for mouse down
const handleMouseDown = (e) => {
setDrawing(true);
const { clientX, clientY } = e;
// Create a new drawing element when mouse down is detected
const element = createElement(clientX, clientY, clientX, clientY);
setElements((prevState) => [...prevState, element]);
};
// Event handler for mouse move
const handleMouseMove = (e) => {
if (!drawing) return;
const { clientX, clientY } = e;
// Find the index of the last element created during mouse down
const index = elements.length - 1;
const { x1, y1 } = elements[index];
// Update the element's coordinates for dynamic drawing
const UpdatedElement = createElement(x1, y1, clientX, clientY);
const elementsCopy = [...elements];
elementsCopy[index] = UpdatedElement;
setElements(elementsCopy);
};
// Event handler for mouse up
const handleMouseUp = () => {
setDrawing(false);
};
// Return JSX to render the collaborative canvas
return (
<>
<canvas
id="canvas"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
width={window.innerWidth}
height={window.innerHeight}
></canvas>
</>
);
};
export default WhiteBoard;
Let's test our application:
The video above shows that our code works and draws lines on our whiteboard using our mouse coordinates.
Drawing Rectangles lines on our Canvas
The process for drawing rectangles on our whiteboard is almost the same as creating lines but only changes when we get to the createElement
function. Before we see the update to our createElement
function, let's create a state that stores the current tool the user intends to utilize.
const [tool, setTool] = useState('line');
By default, the tool is set to line on our canvas. Now we can update our createElement
function to accommodate rectangles.
const createElement = (x1, y1, x2, y2) => {
let roughElement;
// Use the RoughJS generator to create a rough element (line or rectangle)
if (tool === "line") {
roughElement = generator.line(x1, y1, x2, y2);
} else if (tool === "rect") {
roughElement = generator.rectangle(x1, y1, x2 - x1, y2 - y1);
}
// Return an object representing the element, including its coordinates and RoughJS representation
return { x1, y1, x2, y2, roughElement };
};
Now, we need to add buttons to allow the users to select what tool they want to use on our canvas.
return (
<>
<div className="d-flex col-md-2 justify-content-center gap-1">
<div className="d-flex gap-1 align-items-center">
<label htmlFor="line">Line</label>
<input
type="radio"
id="line"
name="tool"
value="line"
checked={tool === "line"}
className="mt-1"
onChange={(e) => setTool(e.target.value)}
/>
</div>
<div className="d-flex gap-1 align-items-center">
<label htmlFor="rect">Rectangle</label>
<input
type="radio"
name="tool"
id="rect"
checked={tool === "rect"}
value="rect"
className="mt-1"
onChange={(e) => setTool(e.target.value)}
/>
</div>
</div>
<canvas
id="canvas"
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
width={window.innerWidth}
height={window.innerHeight}
></canvas>
</>
);
Now, let's test our application:
From the video above, we can see that when we select rectangles, we can draw rectangles on our whiteboard based on the mouse coordinates.
<CTA_Middle_Programming />
Enhancing Interactivity: Enabling Drag and Drop
To offer users the ability to drag and drop elements on the canvas, we'll do the following:
- Introducing Selection Tool: Weโll provide users with a selection tool indicated by a radio button. This tool will allow users to interact with and move existing elements when the button is selected.
<input
type="radio"
id="selection"
checked={tool === "selection"}
onChange={() => setTool("selection")}
/>
<label htmlFor="selection">Drag n Drop</label>
- Detecting Cursor Over an Element: To ascertain if the cursor is over an element, we'll implement a function named
getElementAtPosition
. This function will determine if the cursor is within the boundaries of any existingelements
while the mouse is down.
const distance = (a, b) =>
Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
const getElementAtPosition = (x, y) => {
// Iterate through each element in the 'elements' array
return elements.find((element) => {
const { elementType, x1, y1, x2, y2 } = element;
// Depending on the element type (line or rectangle), perform checks
if (elementType === "rect") {
// Check if the cursor position (x, y) falls within the boundaries of the rectangle
const minX = Math.min(x1, x2);
const maxX = Math.max(x1, x2);
const minY = Math.min(y1, y2);
const maxY = Math.max(y1, y2);
return x >= minX && x <= maxX && y >= minY && y <= maxY;
} else {
// Check if the cursor is close enough to the line using a mathematical offset
const a = { x: x1, y: y1 };
const b = { x: x2, y: y2 };
const c = { x, y };
const offset = distance(a, b) - (distance(a, c) + distance(b, c));
return Math.abs(offset) < 1;
}
});
};
The getElementAtPosition
function takes in the current cursor coordinates (x and y) as parameters. We then use the .find()
method to iterate through the elements array, which contains all the drawing elements on the canvas. We retrieve the elementType
and its current coordinates for each element in the array. If the element is a rectangle, we calculate the minimum and maximum x
and y
values to define the rectangle's boundaries. We then check if the cursor's x
coordinate falls within the range of the rectangle's x
boundaries and if the cursor's y
coordinate falls within the range of the rectangle's y
boundaries. If both conditions are true, the cursor is positioned over the rectangle, so our function returns true. If the element is a line, we calculate the distance between the cursor's coordinates and the line segment defined by the element's x1
, y1
, x2
, and y2
properties. We then compare the calculated offset to a small threshold value (in this case, 1). If the absolute value of the offset is less than the threshold, the cursor is considered to be positioned near the line segment, so our function returns true. The function returns false if the cursor is not positioned over any existing element.
- Storing the Element to Drag: When the user presses the mouse down while the selection tool is active and the cursor is over an element, we'll store that element in a state, along with the initial offset between the cursor and the top-left corner of the element.
const handleMouseDown = (e) => {
const { clientX, clientY } = e;
// Check if the current tool is "Selection"
if (tool === "selection") {
// Find the element at the clicked position
const element = getElementAtPosition(clientX, clientY);
// If an element is found
if (element) {
// Calculate the offset from the top-left corner of the element
const offsetX = clientX - element.x1;
const offsetY = clientY - element.y1;
// Store the selected element along with the offset
setSelectedElement({ ...element, offsetX, offsetY });
// Set the action to "moving" to indicate that dragging is in progress
setAction("moving");
}
} else {
// If the tool is not "Selection", execute code for drawing
// ... (code for drawing)
}
};
- Updating Element Coordinates: In the
handleMouseMove
function, when the user is in the "moving" state (i.e., dragging an element), we calculate the new position of the element based on the mouse cursor's position and the initial offset. We then update the element's coordinates using theupdateElement
function:
const handleMouseMove = (e) => {
const { clientX, clientY } = e;
// Check if the current tool is "Selection"
if (tool === "selection") {
// Determine cursor style based on whether the mouse is over an element
e.target.style.cursor = getElementAtPosition(clientX, clientY, elements)
? "move"
: "default";
}
// Check the current action
if (action === "drawing") {
// ... (code for drawing)
} else if (action === "moving") {
// If in "moving" action (dragging an element)
const { id, x1, x2, y1, y2, elementType, offsetX, offsetY } =
selectedElement;
const width = x2 - x1;
const height = y2 - y1;
// Calculate the new position of the dragged element
const newX = clientX - offsetX;
const newY = clientY - offsetY;
// Update the element's coordinates to perform dragging
const UpdatedElement = createElement(
id,
newX,
newY,
newX + width,
newY + height,
elementType,
);
const elementsCopy = [...elements];
elementsCopy[id] = UpdatedElement;
setElements(elementsCopy);
}
};
Following these steps, we have equipped our canvas with a dynamic drag-and-drop capability. Users can now easily interact with existing elements, moving them across the canvas.
Creating the Real-Time Communication Server with Node.js
A powerful collaboration experience requires a server that seamlessly handles real-time communication between users. To set up our server, we'll do the following:
Install Required Dependencies
Before diving into the Server setup, we must ensure we have the tools in our toolkit. Use the following command to install the required dependencies on our Server:
npm install express cors socket.io
Express
: A popular and flexible Node.js framework that simplifies the process of building robust web applications and APIs. It provides middleware and routing capabilities, making it ideal for creating server-side applications.CORS
(Cross-Origin Resource Sharing): A middleware package that enables Cross-Origin requests. In our case, we'll use it to ensure that our client application (running on a different origin) can interact with the server.Socket.io
: A real-time communication library that facilitates bidirectional communication between clients and the Server. It works over WebSocket connections but also gracefully degrades to other transport mechanisms when necessary.
Configuring Express and Importing Dependencies:
To begin the Server setup, create a file named server.js (or your chosen filename).
touch server.js
We'll then import the dependencies and set up the configuration for Express
:
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 5000;
Configure CORS and Start Server:
To ensure proper cross-origin communication, we'll configure CORS settings and start the Server:
const { createServer } = require("http");
const { Server } = require("socket.io");
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:3000", // Specify your front-end origin
AccessControlAllowOrigin: "http://localhost:3000",
allowedHeaders: ["Access-Control-Allow-Origin"],
credentials: true,
},
});
httpServer.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
In this setup, we've created an Express
app and set up the CORS
configuration to allow communication between our client (running on port 3000) and Server. The socket.io
library is integrated into the httpServer
instance, enabling real-time communication.
Implementing Real-Time Communication.
To implement real-time collaboration between our users we'll have to configure our client (React-app) to connect with our Server by updating our Canvas component with the code below:
const [socket, setSocket] = useState(null);
// useEffect hook to establish and manage the socket connection
useEffect(() => {
// Define the server URL
const server = "http://localhost:5000";
// Configuration options for the socket connection
const connectionOptions = {
"force new connection": true,
reconnectionAttempts: "Infinity",
timeout: 10000,
transports: ["websocket"],
};
// Establish a new socket connection
const newSocket = io(server, connectionOptions);
setSocket(newSocket);
// Event listener for successful connection
newSocket.on("connect", () => {
console.log("Connected to newSocket.io server!");
});
// Event listener for receiving served elements from the server
newSocket.on("servedElements", (elementsCopy) => {
setElements(elementsCopy.elements);
});
// Clean up the socket connection when the component is unmounted
return () => {
newSocket.disconnect();
};
}, []); // Empty dependency array ensures the effect runs only once, during component mount
We will then leverage the event-driven architecture of socket.io
, employing its on
and emit
mechanisms to facilitate seamless data transmission between the client and the server. On the client side, we will enhance the updateElement
function to include a process whereby, with each element update, the data is transmitted to the server.
const updateElement = (id, x1, y1, x2, y2, tool) => {
const UpdatedElement = createElement(id, x1, y1, x2, y2, tool);
const elementsCopy = [...elements];
elementsCopy[id] = UpdatedElement;
setElements(elementsCopy);
socket.emit("elements", elementsCopy);
};
Subsequently, our server will send the received data to other connected clients within the network. This ensures real-time synchronization and collaboration among all participants.
let connections = [];
let elements;
socket.on("elements", (data) => {
elements = data;
connections.forEach((con) => {
if (con.id !== socket.id) {
con.emit("servedElements", { elements });
}
});
});
When the data gets to other clients, we'll then update the elements
state received, causing the useLayoutEffect
to re-render, thus drawing the updated element on the canvas
new socket.on("servedElements", (elementsCopy) => {
setElements(elementsCopy.elements);
});
With this done, whenever one client makes an update, all other clients connected to our Server receive the update. Now, let's test our application:
From the video above, we can see that once one client begins drawing, the other client receives the update and can add to the drawing, allowing for real-time collaboration by all clients on the network.
Conclusion
In this article, we've embarked on an exciting journey of creating a real-time collaboration board powered by the dynamic duo of React.js
and Node.js
, alongside the potent tools of socket.io
and RoughJS
. We've also delved into the realm of seamless teamwork, focusing on sketching lines and rectangles and implementing the drag-and-drop feature on the canvas. Additionally, more shapes and features can be integrated into this project. Beyond what we've already explored, RoughJS
offers a treasure trove of inspiration to enhance your creations. Armed with React.js
, Node.js
, and the insights gained here, you can infuse your projects with the magic of real-time collaboration.