How to use the canvas element in React

The best practices; with hooks and performance

React logo on a canvas

React logo on a canvas

Introduction

Normally, when using vanilla Javascript to manipulate a canvas element, you would have to get an HTMLElement reference of the canvas element in the DOM, then call HTMLCanvasElement.getContext() to get a drawing context and start drawing onto the canvas. However, in React, you use JSX to manipulate the DOM, and when you need access to an HTMLElement reference, you can simply use the useRef hook. Nevertheless, it can be tricky to know where to call useRef and getContext, so they just get called when needed and therefore avoid unnecessary calculations on every render.

The Gist

1. Create a React App

Create a new React app and once all packages are installed, change the directory to the new folder.

npx create-react-app canvas
cd canvas

Finally, you can run the start command and see a new project on localhost:3000

npm run start

New React project on localhost. React logo floating behind a gray background

Small tip

After installing the create-react-app dependencies, I got the following npm audit security warning, regardless I just initialized the app.

27 vulnerabilities (16 moderate, 9 high, 2 critical)

Even though npm audit isn't the best tool to check for security risks, if you don't want to see the warning, you can move react-scripts from dependencies to devDependencies in package.json. However, npm audit still warns for development dependencies by default, so you will have to run npm audit --production to not see the warnings.

// canvas/package.json "dependencies": { "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", "web-vitals": "^1.1.2", }, "devDependencies": { "react-scripts": "4.0.3" },

2. Create and Reference the Canvas Element

Once everything is ready, you can open the src/App.js file and delete all the boilerplate to left just a functional component. Then add a canvas element inside, and import the useRef hook from react and create a reference to the canvas through the ref attribute.

// canvas/src/App.js import {useRef} from "react"; function App() { const canvasRef = useRef(null); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Note

We pass null as the first argument of useRef to use it as the initial value of canvasRef, which is the variable that will store the canvas HTMLElement.

3. Create a Context

You can create a drawing context that is globally available in the component by calling getContext at the top level of the function. We then pass "2d" as the first parameter of getContext, since it defines the context type of the canvas, which can be in two and three dimension.

// canvas/src/App.js import {useRef} from "react"; function App() { const canvasRef = useRef(null); const canvas = canvasRef.current; const context = canvas.getContext("2d"); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Note

The canvas reference isn't stored directly on the canvasRef variable, but in its only property called .current

However, this implementation isn't the most appropriate, since every time the component re-renders, getContext would get called. Although, according to the MDN,

Later calls to the getContext method on the same canvas element, with the same contextType argument, will always return the same drawing context instance as was returned the first time the method was invoked. It is not possible to get a different drawing context object on a given canvas element

So you don't have to worry about your app breaking due to calling several times the getContext method. Nevertheless, you should avoid unnecessary calculations by creating the context inside the useEffect hook. It is valuable to notice that we don't have to add the canvasRef variable to useEffect's dependency array, since mutating a ref doesn't trigger a re-render or a useEffect call, so we left it empty thus it only gets called once.

// canvas/src/App.js import {useRef, useEffect} from "react"; function App() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const context = canvas.getContext("2d"); }, []); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Although, now the context isn't globally available to the component; it only exists inside the useEffect, so to fix that you would have to create a global state with useState and a null initial value. Then inside the useEffect, assign the context to the state.

// canvas/src/App.js import {useRef, useState, useEffect} from "react"; function App() { const [canvasContext, setCanvasContext] = useState(null); const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const context = canvas.getContext("2d"); setCanvasContext(context); }, []); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

4. Draw something!

Finally, we will manipulate the canvas context and canvas element by resizing the canvas to the window size and changing the background color every time the user clicks on the canvas.

If you inspect the canvas element with the dev tools, you will see that the canvas is a small rectangle at the page corner.

Canvas element in the window

To resize it, you can access the window object to get its width and height properties and then use canvasRef.current to change the canvas element size.


jsx
// canvas/src/App.js

import {useRef, useState, useEffect} from "react";

function App() {
	const [canvasContext, setCanvasContext] = useState(null);

	const canvasRef = useRef(null);

	useEffect(() => {
		const windowWidth = window.innerWidth;
		const windowHeight = window.innerHeight;

		const canvas = canvasRef.current;

		canvas.width = windowWidth;
		canvas.height = windowHeight;

		const context = canvas.getContext("2d");
		setCanvasContext(context);
	}, [canvasRef]);

	return (
		<div>
			<canvas ref={canvasRef}></canvas>
		</div>
	);
}

export default App;

To finish, you can use the context variable to manipulate the canvas content inside the onClick canvas' attribute. inside, you can use the context.fillStyle method to change the color of the rectangle, and then use the context.fillRect method to actually draw the rectangle. context.fillRect takes 4 arguments, the two first are the starting x and y coordinates (which will be 0,0), and the last two are the finish point (which will be the canvas width and height).

// canvas/src/App.js //.... return ( <div> <canvas ref={canvasRef} onClick={() => { canvasContext.fillStyle = "red"; canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height); }}></canvas> </div> ); //....

With this, if you click the canvas element, it will change from white to red. Now you can add an array with color names and use Math.random to get a random color every time there is a click.

// canvas/src/App.js import {useRef, useState, useEffect} from "react"; const colors = ["red", "green", "blue", "yellow", "purple", "orange", "black", "white", "brown"]; const getRandomColor = () => { const randomIndex = Math.floor(Math.random() * colors.length); return colors[randomIndex]; }; function App() { const [canvasContext, setCanvasContext] = useState(null); const canvasRef = useRef(null); useEffect(() => { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const canvas = canvasRef.current; canvas.width = windowWidth; canvas.height = windowHeight; const context = canvas.getContext("2d"); setCanvasContext(context); }, [canvasRef]); return ( <div> <canvas ref={canvasRef} onClick={() => { canvasContext.fillStyle = getRandomColor(); canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height); }}></canvas> </div> ); } export default App;

Note

Math.random returns a random value between 0 to 1, so we had to multiply it times the colors array length.

Result

Window randomly switching its background color

Conclusion

As you can see, now you can use the drawing context across the component. It is worth noticing that if you need to use the canvas HTMLElement reference, you can globally use the canvasRef.current property, or the read-only canvasContext.canvas`. I hope you find this approach easy and readable, as well as performant. Until the next time!

Blog

My latest posts

How can I help you?

Learning and improving more and more

Page programmed by me