Building a Collaborative Whiteboard
This guide will walk you through building a collaborative whiteboard using our React SDK and tldraw (opens in a new tab).
Demo
Before we get started, here's a demo of what we'll be building:
Walkthrough
Clone tldraw yjs example app and install dependencies
git clone https://github.com/Huddle01/collaborative-whiteboard
cd collaborative-whiteboard
Install dependencies
pnpm i
Add .env file
cp .env.example .env
Add your own API_KEY
and PROJECT_ID
to the .env
file. You can get these from here (opens in a new tab).
Run the app
pnpm dev
Preparing Whiteboard
Tldraw (opens in a new tab) and yjs
(opens in a new tab) is used to create this collaborative whiteboard.
TLDraw provides easy to use library to create a whiteboard and other infinite canvas experiences.
Yjs
is used to sync the data of whiteboard among other peers.
We took a reference from tldraw-yjs-example (opens in a new tab) to build real-time collaborative whiteboard.
Joining a Room
React SDK is used to enable audio/video functionality and share the data of cursor among other peers to move the viewport of user according to his/her cursor position.
joinRoom
from useRoom
hook is used to join a room, but before that we have to create a room using Create Room API (opens in a new tab).
import { useRoom } from '@huddle01/react/hooks'
const { joinRoom } = useRoom({
onJoin: () => {
console.log('Joined room')
},
})
const handleJoinRoom = async () => {
await joinRoom({
roomId: 'ROOM_ID',
token: "YOUR_ACCESS_TOKEN"
})
}
Handling Local Peer
useLocalPeer
hook is used to update the metadata of local peer. Here, local peer refers the current user who is using the application.
We will update the name of local peer using updateMetadata
method from useLocalPeer
hook.
import { useLocalPeer } from '@huddle01/react/hooks'
type PeerMetadata = {
name: string
}
const { updateMetadata } = useLocalPeer<PeerMetadata>();
const handleUpdateMetadata = () => {
updateMetadata({
name: 'YOUR_NAME'
})
}
useLocalVideo
and useLocalAudio
hooks are used to enable/disable the audio/video functionality of local peer.
import { useLocalVideo, useLocalAudio } from '@huddle01/react/hooks'
const { enableVideo, disableVideo, stream: videoStream, isVideoOn } = useLocalVideo()
const { enableAudio, disableAudio, isAudioOn } = useLocalAudio()
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (stream && videoRef.current) {
videoRef.current.srcObject = stream;
}
}, [stream]);
const handleVideo = () => {
if (isVideoOn) {
disableVideo();
} else {
enableVideo();
}
}
const handleAudio = () => {
if (isAudioOn) {
disableAudio();
} else {
enableAudio();
}
}
return (
<div>
<video ref={videoRef} autoPlay muted />
<button onClick={handleVideo}>Video</button>
<button onClick={handleAudio}>Audio</button>
</div>
)
Handling Remote Peers
useRemotePeer
hook is used to get the metadata of remote peers. Here, remote peers refers to the other users who are in the same room.
This hook requires peerId
which will be unique for each remote peer. You have to create a seperate hook to get the metadata of each remote peer.
You can use usePeerIds
hook to get list of peerIds
of remote peers.
import { useRemotePeer } from '@huddle01/react/hooks'
interface Props {
peerId: string;
}
type PeerMetadata = {
name: string
}
const PeerData = ({ peerId }: Props) => {
// Get the metadata of remote peer
const { metadata } = useRemotePeer<PeerMetadata>({ peerId });
return (
{/* Your UI */}
{metadata.name}
)
};
useRemoteVideo
and useRemoteAudio
hooks are used to enable/disable the audio/video functionality of remote peers.
import { useRemotePeer } from '@huddle01/react/hooks'
import { useRemoteVideo, useRemoteAudio } from '@huddle01/react/hooks'
interface Props {
peerId: string;
}
type PeerMetadata = {
name: string
}
const PeerData = ({ peerId }: Props) => {
// Get the metadata of remote peer
const { metadata } = useRemotePeer<PeerMetadata>({ peerId });
const { stream: videoStream, isVideoOn } = useRemoteVideo({ peerId });
const { stream: audioStream, isAudioOn } = useRemoteAudio({ peerId });
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
if (videoStream && videoRef.current) {
videoRef.current.srcObject = videoStream;
}
}, [stream]);
useEffect(() => {
if (audioStream && videoRef.current) {
audioRef.current.srcObject = audioStream;
}
}, [audioStream]);
return (
{/* Your UI */}
<video ref={videoRef} autoPlay muted />
<audio ref={audioRef} autoPlay>
)
}
Sending Cursor Position Data
We will move the viewport of user according to his/her cursor position. To achieve this we will use sendData
method from useDataMessage
hook
which will send the data of cursor position to remote peers.
import { useDataMessage } from '@huddle01/react/hooks'
import { useEffect, useState } from 'react'
const { sendData } = useDataMessage()
const [cursorPosition, setCursorPosition] = useState({
top: 0,
left: 0,
});
// To track the cursor position we call onMouseMove method inside useEffect hook
useEffect(() => {
const onMouseMove = (e: MouseEvent) => {
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const cursorWidth = 200; // adjust as needed
const cursorHeight = 150; // adjust as needed
// Adjust the cursor position to stay within the screen
const adjustedTop = Math.min(e.clientY, screenHeight - cursorHeight);
const adjustedLeft = Math.min(e.clientX, screenWidth - cursorWidth);
setCursorPosition({
top: adjustedTop + 15,
left: adjustedLeft + 15,
});
// Send the cursor position data to remote peers
sendData({
to: '*',
payload: JSON.stringify({
top: adjustedTop + 15,
left: adjustedLeft + 15,
}),
label: 'cursor',
});
};
document.addEventListener('mousemove', onMouseMove);
return () => {
document.removeEventListener('mousemove', onMouseMove);
};
}, []);
Receiving Cursor Position Data
To receive the data of cursor position from remote peers we will use onMessage
prop from useDataMessage
hook.
import { useDataMessage } from '@huddle01/react/hooks'
import { useState } from 'react'
// State to store the cursor position data
const [cursorPosition, setCursorPosition] = useState({
top: 0,
left: 0,
});
// Receive the cursor position data from remote peers
useDataMessage({
onMessage(payload, from, label) {
if (label === 'cursor' && from === peerId) {
const { top, left } = JSON.parse(payload);
setCursorPosition({
top: top,
left: left,
});
}
},
});
return (
<div
style={{
position: 'absolute',
...cursorPosition,
}}
>
{/* Code to show viewport of remote peer which will move with cursor */}
</div>
)
You're all set! Happy Hacking! 🎉
For more information, please refer to the React SDK Reference.