Refs in React have gone through several changes since their introduction. With the release of hooks in React 16.8, useRef became a handy way to interact with refs, especially in functional components. However, as TypeScript has come to dominate the JS landscape, it's been a bit of a pain dealing with refs while still maintaining types. This is exacerbated by working with HTML5 Canvas as it is primarily interacted with through JavaScript apis. Without preserving Typing, working with HTML 5 Canvas is cumbersome.
In this post, I'll give a very brief overview of working with HTML5 Canvas and Refs in TypeScript.
Part 1: Rendering the Canvas Element
If you depend heavily on UI libraries, it may confusing how to render an HTML5 Canvas element. However, like any other HTML tag, React recognizes the <canvas>
tag (lowercase) as a valid HTML element.
As such, creating a Canvas element is as simple as:
const SimpleCanvasExample: React.FC<{}> = () => {
return <canvas></canvas>;
};
Using the inspector, we can see that a <canvas>
tag is rendered.
This doesn't get us much as is. Just like an img tag, we need to do more than merely output the tag to make it useful. Unlike an img tag, however, we will need to do that extra work in JavaScript rather than in JSX markup.
Part 2: Using the useRef hook to access the Canvas Element
In much of React development, the elements being output don't need to be interacted with. React handles all the loading, rendering, and unloading of the elements. It is rarely something to consider with most React use cases. However, due to the need to manipulate the Canvas tag with JavaScript code, we will need someway to access the actual <canvas>
DOM Element.
This is where React's refs come into play. A ref in React is a reference to the underlying value, typically a Dom Element. Using the useRef
hook, the example can be updated to:
const SimpleCanvasExample: React.FC<{}> = () => {
const canvasRef = useRef(null);
return <canvas ref={canvasRef}></canvas>;
};
With this slight change, the <canvas />
DOM Element is available via canvasRef.current
.
Style Note: I tend to end all of my ref variables with Ref so it is obvious that a variable refers to a ref. This can be annoying to keep track of especially when assigning the current ref to temp variables.
Part 3: Making React Refs Type aware with TypeScript Generics.
TypeScript cannot infer the type of a ref since it can really be any value. As such, we lose the benefit of TypeScript as well as the IDE's autocompletion:
Luckily, React supports TypeScript Generics for useRef
. The TypeScript type for the HTML5 Canvas element is HTMLCanvasElement
. We can pass this type into the useRef Generic to inform TypeScript we are working explicitly with Canvas elements:
// Un Typed useRef usage
const canvasRef = useRef(null);
// Typed useRef usage
const canvasRef = useRef<HTMLCanvasElement | null>(null);
This also enables property autocompletion:
Part 4: Accessing and Typing the Canvas Context
An HTML5 Canvas is primarily manipulated though its Context. For the most part, the Canvas DOM Element isn't that useful once we have the context - it's just another ol' DOM Element at that point. It is the Context that provides much of the api we need to interact with.
Since an HTML Canvas can be 2d or 3d, we need to explicitly tell the Canvas what type it is by way of the context. For this example, we'll deal strictly with 2d canvas:
canvasRef.current.getContext('2d')
The TypeScript type of the return object is CanvasRenderingContext2D
.
As a convenience, we can also store the context even though it isn't a DOM element explicitly. This is a not very well known feature of refs.
Leveraging TypeScript Generics as per above, we get the following:
const canvasCtxRef = React.useRef<CanvasRenderingContext2D | null>(null);
Part 5: Instantiating Context and Manipulating Canvas
The context can only be instantiated once the Canvas DOM Element is ready. As such, we can utilize the useEffect
hook.
useEffect(() => {
// Initialize
if (canvasRef.current) {
canvasCtxRef.current = canvasRef.current.getContext('2d');
}
}, []);
Note: Since canvasRef.current can technically be null for legit reasons, I wrapped it in the conditional. Alternately, you can use the Non Null Assertion Operator, which I use below when the ref.current is surely not undefined and I want TS to disregard.
Finally, we can do some basic HTML Canvas manipulations.
useEffect(() => {
// Initialize
if (canvasRef.current) {
canvasCtxRef.current = canvasRef.current.getContext('2d');
let ctx = canvasCtxRef.current; // Assigning to a temp variable
ctx!.beginPath(); // Note the Non Null Assertion
ctx!.arc(95, 50, 40, 0, 2 * Math.PI);
ctx!.stroke();
}
}, []);
This produces the following result:
The full final component looks like:
import React, { useRef, useEffect } from 'react';
const SimpleCanvasExample: React.FC<{}> = () => {
let canvasRef = useRef<HTMLCanvasElement | null>(null);
let canvasCtxRef = React.useRef<CanvasRenderingContext2D | null>(null);
useEffect(() => {
// Initialize
if (canvasRef.current) {
canvasCtxRef.current = canvasRef.current.getContext('2d');
let ctx = canvasCtxRef.current;
ctx!.beginPath();
ctx!.arc(95, 50, 40, 0, 2 * Math.PI);
ctx!.stroke();
}
}, []);
return <canvas ref={canvasRef}></canvas>;
};
export default SimpleCanvasExample;
Wrapping Up
Interacting with HTML5 Canvas can be a pain by itself, but adding React to the mix can be even more confusing. However, by leveraging React Refs with TypeScript Generics, working with Canvas can be a breeze. You can see the running example in my newly launched "Code Lab". I will be posting more interesting examples as time goes on. I hope you enjoy.
Discussion Topics
- In what scenarios do you find yourself using React Refs?
- What are some of your favorite HTML 5 Canvas experiments?
Image Credit: Pink Daylily by Burst on Pexels