Building a Collaborative Whiteboard using ReactJS, Socket.io and NodeJS 🤝
Tags: webdev, javascript, tutorial, react
Published: 2023-12-17 13:41:56

Hi fellow readers! I hope you’re doing great. In this article, we will build our collaborative whiteboard similar to Google Meet’s whiteboard. We are going to use **HTML canvas** with **ReactJS** to create our whiteboard and **socket.io** to implement real-time communication. 

Here’s how the project is gonna look when implemented. Let’s get started.


![Project whiteboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay12ya5pqcj3wqc8isci.gif)

In the project directory, we will create folders, and create the frontend using the command `npm create vite@latest` and choose ‘react’ and ‘typescript’.

## Create Canvas

In the front end, create a component Board. This component will include our Canvas and its functionalities.

```react
import React, { useRef } from 'react';

const Board = () => {

	const canvasRef = useRef<HTMLCanvasElement>(null);


    return (
        <canvas
            ref={canvasRef}
            width={600}
            height={400}
            style={{ backgroundColor: 'white' }}
        />
    );
};


export default Board;
```

Here, we create a reference of the current state of the canvas using **useRef** hook which is now known as **canvasRef**. We have kept the background color to be white to create a whiteboard environment, it could be any color you want or none at all.

## Draw on canvas

Now, we created the canvas, let’s see how we can draw/write on it. 

```react
    useEffect(() => {


        // Variables to store drawing state
        let isDrawing = false;
        let lastX = 0;
        let lastY = 0;


        const startDrawing = (e: { offsetX: number; offsetY: number; }) => {
            isDrawing = true;


            [lastX, lastY] = [e.offsetX, e.offsetY];
        };


        // Function to draw
        const draw = (e: { offsetX: number; offsetY: number; }) => {
            if (!isDrawing) return;


            const canvas = canvasRef.current;
            const ctx = canvas.getContext('2d');
            if (ctx) {
                ctx.beginPath();
                ctx.moveTo(lastX, lastY);
                ctx.lineTo(e.offsetX, e.offsetY);
                ctx.stroke();
            }


            [lastX, lastY] = [e.offsetX, e.offsetY];
        };


        // Function to end drawing
        const endDrawing = () => {
            isDrawing = false;
        };


        const canvas: HTMLCanvasElement | null = canvasRef.current;
        const ctx = canvasRef.current?.getContext('2d');


        // Set initial drawing styles
        if (ctx) {
            ctx.strokeStyle = black;
            ctx.lineWidth = 5;


            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';


        }
        // Event listeners for drawing
        canvas.addEventListener('mousedown', startDrawing);
        canvas.addEventListener('mousemove', draw);
        canvas.addEventListener('mouseup', endDrawing);
        canvas.addEventListener('mouseout', endDrawing);


        return () => {
            // Clean up event listeners when component unmounts
            canvas.removeEventListener('mousedown', startDrawing);
            canvas.removeEventListener('mousemove', draw);
            canvas.removeEventListener('mouseup', endDrawing);
            canvas.removeEventListener('mouseout', endDrawing);
        };
    }, []);

```

We are managing different mouse events on our canvas using JavaScript event listeners. As soon as the mouse is right-clicked on canvas, we perform the **startDrawing()** function, and when the mouse moves, **draw()** is invoked, and as soon as we leave the mouse click, the **endDrawing()** function is called. Let’s see this in detail.

We use the `ctx` variable to get the current canvas reference and perform actions on it. We have kept some default properties of our **ctx** variable, such as **strokeStyle** to be black, **lineWidth** is 5, **lineCap**, and **lineJoin** to be round.

 We setup the event listeners on Canvas and also clean up the event listeners when the component unmounts or runs before the next effect. We are using `isDrawing`, `lastX`, and `lastY` variables to control the drawing state. Initially, isDrawing is set to false, until mousedown occurs. It changes the state of **isDrawing** to `true`. When the mouse moves, the `ctx` variable creates strokes according to the position of our cursor on the canvas. When we stop the right click, or move the cursor out of the canvas, the **endDrawing()** function is called which sets the **isDrawing** to false. With this, we can freely draw on canvas :)

## Backend

Now, let’s add the feature to allow multiple users to use the whiteboard at the same time. Go to the backend directory, first, execute the command
`npm install socket.io`, and code the following in an _index.js_ file.

```javascript
const { Server } = require('socket.io');
const io = new Server({
    cors: "http://localhost:5173/"
})
io.on('connection', function (socket) {


    socket.on('canvasImage', (data) => {
        socket.broadcast.emit('canvasImage', data);
    });
});


io.listen(5000);
```

Here, we have initialized a new Socket.IO server instance named io. The cors option is set to allow connections from the specified URL (http://localhost:5173/), which is our front-end URL. If we don’t put this, our frontend will not be able to share resources with the backend.

The `io.on(‘connection’)` sets up an event listener for whenever a client connects to the Socket.IO server.  Inside this, we have another event listener listening to the event ‘canvasImage’ from the front end via users. 
When a user emits an event from the front end, it will contain the most recent copy of the canvas. We will receive the data using this event and then `socket.broadcast.emit(‘canvasImage’, data)` broadcasts the received _canvasImage_ data to all connected clients except the client that sent the original 'canvasImage' event. Last, but not least, we start our server using the command `node index.js` which runs on port **5000**.

## Client Socket Connection

In _Board.tsx_, we will now make some changes. Before that, we’ll install _'socket.io-client'_ package in the frontend directory. 


```react
import React, { useRef, useEffect, useState } from 'react';
import io from 'socket.io-client';
    
const [socket, setSocket] = useState(null);

    useEffect(() => {
        const newSocket = io('http://localhost:5000');
        console.log(newSocket, "Connected to socket");
        setSocket(newSocket);
    }, []);


    useEffect(() => {

        if (socket) {
            // Event listener for receiving canvas data from the socket
            socket.on('canvasImage', (data) => {
                // Create an image object from the data URL
                const image = new Image();
                image.src = data;


                const canvas = canvasRef.current;
                // eslint-disable-next-line react-hooks/exhaustive-deps
                const ctx = canvas.getContext('2d');
                // Draw the image onto the canvas
                image.onload = () => {
                    ctx.drawImage(image, 0, 0);
                };
            });
        }
    }, [socket]);
```

We’ll create a new useState hook named socket for managing sockets. 
`const newSocket = io('http://localhost:5000')` establishes a connection with the server and client. 

To get updates from all users using the whiteboard, we are listening for the **‘canvasImage’** event from the server using the useEffect hook. If we receive an event, we are going to draw that image on our current canvas. 

So, shall we send the image to the server? Every time we draw? No, we send the image to the server when we are drawn i.e. once, we lift our right-click. Here’s the updated **endDrawing()** function.

```react
        const endDrawing = () => {
            const canvas = canvasRef.current;
            const dataURL = canvas.toDataURL(); // Get the data URL of the canvas content


            // Send the dataURL or image data to the socket
            // console.log('drawing ended')
            if (socket) {
                socket.emit('canvasImage', dataURL);
                console.log('drawing ended')
            }
            isDrawing = false;
        };
```

Simple! With this, we are done with a collaborative whiteboard. But, if you want an option to adjust the color and size of the brush. Hold on, we are going to do it next. 

## Adjust Color and Size

We are going to use two states for controlling these two brush properties. In _App.tsx_, we are going to use **onChange()** function on the inputs to update each state.

```react
import { useEffect, useState } from 'react';
import './App.css';
import Board from './component/Board';

const CanvasDrawing = () => {

  const [brushColor, setBrushColor] = useState('black');
  const [brushSize, setBrushSize] = useState<number>(5);

  return (
    <div className="App" >
      <h1>Collaborative Whiteboard</h1>
      <div>
        <Board brushColor={brushColor} brushSize={brushSize} />
        <div className='tools' >
          <div>
            <span>Color: </span>
            <input type="color" value={brushColor} onChange={(e) => setBrushColor(e.target.value)} />
          </div>
          <div>
            <span>Size: </span>
            <input type="range" color='#fac176'
              min="1" max="100" value={brushSize} onChange={(e) => setBrushSize(Number(e.target.value))} />
            <span>{brushSize}</span>
          </div>
        </div>
      </div>
    </div>
  );
};

export default CanvasDrawing;
```

Let’s add some CSS in App.css to make it look a bit more pretty.

```css
.App {
    background-color: #4158D0;
    background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%);
    height: 100vh;
    width: 100%;
    padding: 3em 1em;
    display: flex;
    flex-direction: column;
    align-items: center;
}


.tools {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    align-items: center;
    width: 100%;
    padding: 1em;
    background-color: black;
    color: white;
}


h1 {
    margin-bottom: 1rem;
    text-align: center;
}


input {
    margin: 0 5px;
}


span {
    font-size: 1.3em;
}




@media screen and (max-width: 600px) {
    .tools {
        flex-direction: column;
        align-items: start;
    }


}
```
Index.css

```css
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');


* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Roboto', sans-serif;
}
```

We are passing **brushSize** and **brushColor** states as props to the Board component.


```react
interface MyBoard {
    brushColor: string;
    brushSize: number;
}


const Board: React.FC<MyBoard> = (props) => {

    const { brushColor, brushSize } = props;

    useEffect(() => {

// Variables to store drawing state
        let isDrawing = false;
        let lastX = 0;
        let lastY = 0;
        const startDrawing = (e: { offsetX: number; offsetY: number; }) => {
            isDrawing = true;


            console.log(`drawing started`, brushColor, brushSize);
            [lastX, lastY] = [e.offsetX, e.offsetY];
        };


        // Function to draw
        const draw = (e: { offsetX: number; offsetY: number; }) => {
            if (!isDrawing) return;


            const canvas = canvasRef.current;
            const ctx = canvas.getContext('2d');
            if (ctx) {
                ctx.beginPath();
                ctx.moveTo(lastX, lastY);
                ctx.lineTo(e.offsetX, e.offsetY);
                ctx.stroke();
            }


            [lastX, lastY] = [e.offsetX, e.offsetY];
        };


        // Function to end drawing
        const endDrawing = () => {
            const canvas = canvasRef.current;
            const dataURL = canvas.toDataURL(); // Get the data URL of the canvas content


            // Send the dataURL or image data to the socket
            // console.log('drawing ended')
            if (socket) {
                socket.emit('canvasImage', dataURL);
                console.log('drawing ended')
            }
            isDrawing = false;
        };


        const canvas: HTMLCanvasElement | null = canvasRef.current;
        const ctx = canvasRef.current?.getContext('2d');


        // Set initial drawing styles
        if (ctx) {
            ctx.strokeStyle = brushColor;
            ctx.lineWidth = brushSize;


            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';


        }
        // Event listeners for drawing
        canvas.addEventListener('mousedown', startDrawing);
        canvas.addEventListener('mousemove', draw);
        canvas.addEventListener('mouseup', endDrawing);
        canvas.addEventListener('mouseout', endDrawing);


        return () => {
            // Clean up event listeners when component unmounts
            canvas.removeEventListener('mousedown', startDrawing);
            canvas.removeEventListener('mousemove', draw);
            canvas.removeEventListener('mouseup', endDrawing);
            canvas.removeEventListener('mouseout', endDrawing);
        };
    }, [brushColor, brushSize, socket]);
```

In the useEffect, we are making changes `ctx.strokeStyle = brushColor;` and  `ctx.lineWidth = brushSize;` and adding brushColor, brushSize, socket in the dependency array.


## Responsive Canvas

We are going to make our canvas responsive with a bit of JavaScript. The window object to fetch the width and height of the device and accordingly, we will show the canvas. 

```react
    const [windowSize, setWindowSize] = useState([
        window.innerWidth,
        window.innerHeight,
    ]);


    useEffect(() => {
        const handleWindowResize = () => {
            setWindowSize([window.innerWidth, window.innerHeight]);
        };


        window.addEventListener('resize', handleWindowResize);


        return () => {
            window.removeEventListener('resize', handleWindowResize);
        };
    }, []);




    return (
        <canvas
            ref={canvasRef}
            width={windowSize[0] > 600 ? 600 : 300}
            height={windowSize[1] > 400 ? 400 : 200}
            style={{ backgroundColor: 'white' }}
        />
    );
```

We are using _‘resize’_ event listener to update the state of the width and height of the windowSize state. And finally, we have completed our web app.

I hope you liked this project.
Thank you for reading!

If you have any doubts or wanna get a peek at the source code. You can check it out [here](https://github.com/FidalMathew/collaborative_whiteboard).

**Connect with me on-**

* [Twitter](https://twitter.com/fidalmathew10)
* [Github](https://github.com/FidalMathew)
* [Linkedin](https://www.linkedin.com/in/fidalmathew/)