How to use HTML Canvas with React Hooks

MasaKudamatsu
Dec 9, 2020 · 7 min read

(If you’re not a Medium member, click here to read this article.)

I’m making a color picker web app with React. Drawing a raster image like the color picker on the web requires a <canvas> HTML element. But the HTML canvas and React do not easily go hand in hand.

I’ve found a bunch of web articles on the topic, most of which are outdated as they use React class components. Those with React hooks are helpful but not fully accurate. So it took quite a while for me to make it work in my web dev project.

To help you (and my future self) save time to set up a canvas element in React app, let me share the definitive version of how to use the HTML canvas with React hooks (with a link to my demo).

TL;DR

First, create a React component out of the <canvas> element:

// src/components/Canvas.jsimport React from 'react';
import PropTypes from 'prop-types';
const Canvas = ({draw, height, width}) => {
const canvas = React.useRef();
React.useEffect(() => {
const context = canvas.current.getContext('2d');
draw(context);
});
return (
<canvas ref={canvas} height={height} width={width} />
);
};
Canvas.propTypes = {
draw: PropTypes.func.isRequired,
height: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
};
export default Canvas;

Then, use this component with the props referring to the function to draw an image (draw) and to the image resolution and aspect ratio (width and height):

// src/App.jsimport Canvas from './components/Canvas';const draw = context => {
// Insert your canvas API code to draw an image
};
function App() {
return (
<Canvas draw={draw} height={100} width={100} />
);
}
export default App;

Demo for the above code is available at my CodeSandbox.

Below I break down the above code into 6 steps, to help you understand what is going on. ;-)

NOTE: To learn how to draw an image with the canvas element, I recommend MDN’s tutorial (MDN Contributors 2019).

Step 1: Render a canvas element

// src/components/Canvas.jsimport React from 'react';const Canvas = () => {
return (
<canvas
width="100"
height="100"
/>
)
};
export default Canvas;

The width and height attributes determine two things about the image created by the canvas element: the image resolution and the aspect ratio.

Image resolution

In the above example, the image has 100 x 100 pixels. In this case, drawing a line thinner than 1/100 of the image width ends up in sub-pixel rendering, which should be avoided for the performance reason (see MDN Contributors 2019b). If the thinnest line is, say, 1/200 of the image width, then you should set width="200".

Aspect ratio

The above example also defines the aspect ratio of the image as 1 to 1 (i.e. a square). If we fail to specify the width and height attributes (as so many articles on HTML canvas do), the default aspect ratio of 2:1 (300px wide and 150px high) will apply. This can cause a stretched image, depending on how you style it with CSS (see MDN Contributors 2019a). Corey's (2019) helpful article on how to use React hooks to render a canvas element appears to fall this trap by failing to specify width and height attributes.

Up until now, it has nothing to do with React. Anytime you use the HTML canvas, you should set width and height attributes.

Step 2: Refer to the canvas element

To draw an image with a <canvas> element, you first need to refer to it in JavaScript. An introductory tutorial to the HTML canvas (e.g. MDN Contributors 2019a) tells you to use document.getElementById(id) where id is the id attribute value of the canvas element.

In React, however, using the useRef hook is the way to go (see Farmer 2018 for why).

Create a variable pointing to useRef(), and then use this variable as the value of the ref attribute of the canvas element:

// src/components/Canvas.jsimport React from 'react'; const Canvas = () => {
const canvas = React.useRef(); // ADDED
return (
<canvas
ref={canvas} // ADDED
width="100"
height="100"
/>
)
};
export default Canvas;

This way, once the canvas element is rendered on the screen, we can refer to it as canvas.current in our JavaScript code. See React (2020a) for more detail.

Step 3: Create the canvas context

To draw an image in the canvas element, you then need to create the CanvasRenderingContext2D object (often assigned a variable name like context or ctx in the code).

This step is the trickiest part of using the HTML canvas with React. The solution is the useEffect hook:

// src/components/Canvas.jsimport React from 'react';const Canvas = () => {
const canvas = React.useRef();
// ADDED
React.useEffect(() => {
const context = canvas.current.getContext('2d');
});
return (
<canvas
ref={canvas}
width="100"
height="100"
/>
)
};
export default Canvas;

As explained in the previous step, the canvas.current refers to the <canvas> element in the above code. But it's null until React actually renders the canvas element on the screen. To run a set of code after React renders a component, we need to enclose it with the useEffect hook (see West 2019 for when the useEffect code block runs during the React component life cycle).

Within its code block, therefore, the canvas.current does refer to the <canvas> element. This is the technique I've learned from Corey (2019), Nanda 2020 and van Gilst (2019).

Step 4: Draw an image

Now we’re ready to draw an image with various methods of the context object (see MDN Contributors 2020).

To reuse the code that we have written so far, however, it’s best to separate it from the code for drawing an image. So we pass a function to draw an image as a prop to the Canvas component (I borrow this idea from Nanda 2020):

// src/components/Canvas.jsimport React from 'react';
import PropTypes from 'prop-types'; // ADDED
const Canvas = ( {draw} ) => { // CHANGED
const canvas = React.useRef();
React.useEffect(() => {
const context = canvas.current.getContext('2d');
draw(context); // ADDED
});
return (
<canvas
ref={canvas}
width="100"
height="100"
/>
)
};
// ADDED
Canvas.propTypes = {
draw: PropTypes.func.isRequired,
};
export default Canvas;

The draw() function draws the image, to be defined in another file. To access to various drawing methods, it takes context as its argument.

As the Canvas component now takes props, I add PropTypes to make explicit the data type of each prop (see React 2020b).

Step 5: Make the component reusable

Now if we want to reuse this Canvas component, we do not want to hard-code its width and height attributes. Different images have different resolutions and aspect ratios.

So convert these two values into additional props:

// src/components/Canvas.jsimport React from 'react';
import PropTypes from 'prop-types';
const Canvas = ( {draw, height, width} ) => { // CHANGED
const canvas = React.useRef();
React.useEffect(() => {
const context = canvas.current.getContext('2d');
draw(context);
});
return (
<canvas
ref={canvas}
width={width} // CHANGED
height={height} // CHANGED
/>
)
}
// ADDED
Canvas.propTypes = {
draw: PropTypes.func.isRequired,
height: PropTypes.number.isRequired, // ADDED
width: PropTypes.number.isRequired, // ADDED
};
export default Canvas;

One benefit of using PropTypes is that, by adding .isRequired, we will be alerted in the console in case we forget setting the prop values. As mentioned above (see Step 1), the width and height attributes are best specified for performance and for avoiding image distortion. With the above code, we will be alerted when we forget specifying their values.

Step 6: Render the canvas component

Finally, in a parent component, render the Canvas component together with specifying the draw() function:

// src/App.js import React from 'react';
import Canvas from './components/Canvas'; // Change the path according to the directory structure of your project
const draw = context => {
// Insert your code to draw an image
};
function App() {
return (
<Canvas draw={draw} height={100} width={100} />
);
}
export default App;

Demo

Check out how it actually works with my CodeSandbox demo.

Hope this article and the above demo help you kickstart drawing canvas images with React in your web app project!

This article is part of Web Dev Survey from Kyoto, a series of my blog posts on web development. It intends to simulate that the reader is invited to Kyoto, Japan, to attend a web dev conference. So the article ends with a photo of Kyoto in the current season, as if you were sightseeing after the conference was over. :-)

Today I take you to the entrance garden of Seigen-in, a sub-temple of Ryoan-ji of the rock garden fame:

Seigen-ji Sub-temple Entrance Garden at 9:54 am on 1 December, 2020. Photographed by Masa Kudamatsu (the author of this article)

Hope you have learned something today! Happy coding!

Footnote

I use the Author-Date referencing system in this article, to refer to various articles on web development.

References

Corey (2019) “Animating a Canvas with React Hooks”, petecorey.com, Aug. 19, 2019.

Farmer, Andrew H. (2018) “Why to use refs instead of IDs”, JavaScript Stuff, Jan 27, 2018.

MDN Contributors (2019a) “Basic usage of canvas”, MDN Web Docs, Dec 5, 2019.

MDN Contributors (2019b) “Optimizing canvas”, MDN Web Docs, Apr 7, 2019.

MDN Contributors (2019c) “Canvas tutorial”, MDN Web Docs, Dec 1, 2019.

MDN Contributors (2020) “Drawing shapes with canvas”, MDN Web Docs, Aug 5, 2020.

Nanda, Souradeep (2020) “An answer to ‘Rendering / Returning HTML5 Canvas in ReactJS’”, Stack Overflow, Aug 2, 2020.

React (2020a) “Hooks API Reference”, React Docs, Mar 9, 2020.

React (2020b) “Typechecking with PropTypes”, React Docs, Nov 25, 2020.

van Gilst (2019) “Using React Hooks with canvas”, blog.koenvangilst.nl, Mar 16, 2019.

West, Donavon (2019) “React Hook Flow Diagram”, GitHub, Mar 12, 2019.

Web Dev Survey from Kyoto

A former academic researcher weaves web dev articles into a story, with a visit to Kyoto at the end.

MasaKudamatsu

Written by

Designer-developer of Triangulum Color Picker (https://triangulum.netlify.app/) and Line-height Picker (https://line-height-picker.app/).

Web Dev Survey from Kyoto

A virtual web dev conference held in Kyoto, with a former academic researcher using his “literature review” skill to collect scattered pieces of knowledge on web development into a logical, easy-to-understand document.

MasaKudamatsu

Written by

Designer-developer of Triangulum Color Picker (https://triangulum.netlify.app/) and Line-height Picker (https://line-height-picker.app/).

Web Dev Survey from Kyoto

A virtual web dev conference held in Kyoto, with a former academic researcher using his “literature review” skill to collect scattered pieces of knowledge on web development into a logical, easy-to-understand document.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store