Skip to content

Chapter 7 - Draw simultaneously on multiple windows

In the previous chapter, you were able to validate your frontend and your backend can communicate.

In this chapter, you'll update the WebSocket provider to send the drawing data to the WebSocket gateway. This will allow you to draw simultaneously on multiple windows!

Steps

Update the WebSocket provider

Update the WebSocket provider to emit and receive WebSocket events to and from the backend.

frontend/src/components/websocket-provider.tsx
import { createContext, useContext, useEffect, useMemo } from "react";
import { Socket, io } from "socket.io-client";
import { useLines } from "@/components/line-provider";
import { Point } from "@/types/point";

type State = { // (1)!
	emitFirstPointFromPlayer: Function,
	emitPointFromPlayer: Function
};

const WebSocketContext = createContext<State | undefined>(undefined);

function WebSocketProvider({ children }: { children: React.ReactNode }): JSX.Element {
	const socket: Socket = useMemo(
		() => io("localhost:4000", { autoConnect: false }),
		[]
	);
	const { dispatch } = useLines();

	useEffect(() => {
		if (!socket.connected) {
			socket.connect();
		}
		return () => {
			socket.close();
		};
	}, [socket]);

	useEffect(() => { // (2)!
		if (!socket) return;

		socket.on("FIRST_POINT_TO_PLAYERS", (point: Point) => { // (3)!
			dispatch({ type: "ADD_FIRST_POINT", point: point });
		});

		socket.on("POINT_TO_PLAYERS", (point: Point) => { // (4)!
			dispatch({ type: "ADD_POINT", point: point });
		});
	}, [socket, dispatch]);

	const emitFirstPointFromPlayer = (point: Point) => { // (5)!
		socket.emit("FIRST_POINT_FROM_PLAYER", point);
	};

	const emitPointFromPlayer = (point: Point) => { // (6)!
		socket.emit("POINT_FROM_PLAYER", point);
	};

	return (
		<WebSocketContext.Provider value={{ emitFirstPointFromPlayer, emitPointFromPlayer }}>
			{children}
		</WebSocketContext.Provider>
	);
}

function useWebSocket(): State {
	const context = useContext(WebSocketContext);
	if (context === undefined) {
		throw new Error("useWebSocket must be used within a WebSocketProvider");
	}
	return context;
}

export { WebSocketProvider, useWebSocket };
  1. The state that the WebSocketProvider provides are two functions to send data to the backend.
  2. A second useEffect function allows to keep the code clean. This useEffect is responsible to react to WebSocket events sent from the backend.
  3. On the event FIRST_POINT_TO_PLAYERS, the point is dispatched to the LineProvider using its dispatch method.
  4. On the event POINT_TO_PLAYERS, the point is dispatched to the LineProvider using its dispatch method.
  5. Method to emit the start of a new line to the backend. The backend will then broadcast the message to the other players.
  6. Method to emit the next point of a line to the backend. The backend will then broadcast the message to the other players.

Update the sketch

Update the SketchComponent to emit the points made by the player using the WebSocket provider.

frontend/src/sketch.tsx
import React from "react";
import Konva from "konva";
import { Stage, Layer, Line } from "react-konva";
import { useLines } from "@/components/line-provider";
import { useWebSocket } from "@/components/websocket-provider";

export const Sketch = () => {
	const { state: lines, dispatch: dispatchLines } = useLines();
	const { emitFirstPointFromPlayer, emitPointFromPlayer } = useWebSocket();
	const isDrawing = React.useRef(false);

	const getPointFromMouseEvent = (
		mouseEvent: Konva.KonvaEventObject<MouseEvent>
	) => {
		return mouseEvent.target.getStage()?.getPointerPosition() ?? { x: 0, y: 0 };
	};

	const handleMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {
		isDrawing.current = true;
		const point = getPointFromMouseEvent(e);
		dispatchLines({ type: "ADD_FIRST_POINT", point: point });
		emitFirstPointFromPlayer(point);
	};

	const handleMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {
		if (!isDrawing.current) {
			return;
		}
		const point = getPointFromMouseEvent(e);
		dispatchLines({ type: "ADD_POINT", point: point });
		emitPointFromPlayer(point);
	};

	const handleMouseUp = () => {
		isDrawing.current = false;
	};

	return (
		<div>
			<Stage
				width={window.innerWidth}
				height={window.innerHeight}
				onMouseDown={handleMouseDown}
				onMousemove={handleMouseMove}
				onMouseup={handleMouseUp}
			>
				<Layer>
					{lines.map((line, i) => (
						<Line
							key={i}
							points={line}
							stroke="#df4b26"
							strokeWidth={5}
							tension={0.5}
							lineCap="round"
							lineJoin="round"
						/>
					))}
				</Layer>
			</Stage>
		</div>
	);
};

Check the results

Start the frontend and the backend services. Access the frontend on http://localhost:3000. You should see the exact same result as earlier.

Open another window and access http://localhost:3000.

Start to draw on one window. You should see the drawing made on the second window as well!

Summary

Congrats! You have now a simultaneous drawing application! Very soon, you'll be able to draw collaboratively from multiple devices!