Masa Kudamatsu
Posted on December 9, 2020
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.js
import 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.js
import 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.js
import 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.js
import 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.js
import 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.js
import 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.js
import 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.
Posted on December 9, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.