The <canvas/> HTML element can be used to draw graphics with a finer control than the usual DOM or SVG. But with React, trying to draw on a canvas is not intuitive as their interfaces are quite different. With React, each component owns their node, as opposed to canvas where there is only one shared node that we can use for drawing. Let's see how we can make a canvas visualization with React components !

Link icon 🧑‍🏫 Canvas 101

The canvas element is like a sheet of paper. To draw in real life you would take a pen, move your hand to a first position, and draw a line by moving your hand to another position. The browser API to draw on a canvas is actually very similar. We first need to make a blueprint of the shape we want to draw – like using a real pencil – that can later be colored in.

canvasContext . moveTo ( x1 , y1 ) ; canvasContext . lineTo ( x2 , y2 ) ; canvasContext . strokeStyle = "purple" ; canvasContext . stroke ( ) ;

Having imperative code like this in a component-oriented codebase can be tricky! We would need to create a component that renders a <canvas/> on the page and then call the moveTo and lineTo methods on it to draw a line. In practice, it's a bit more complicated to bridge those two. We need to use a React reference to access the canvas DOM node, and to retrieve a 2D context from it; we are then able to call our drawing methods. The code would look like this:

const Canvas = ( ) => { const canvasRef = React . useRef ( null ) ; React . useEffect ( ( ) => { const context = canvasRef . current . getContext ( "2d" ) ; } , [ canvasRef ] ) ; return < canvas ref = { canvasRef } /> ; } ; CodeSandbox icon Edit on CodeSandbox

But if we want to draw something a bit more complex, the Canvas component can become quite large. Usually, big components are split into several child components. Yet here this is not possible as there is only one canvas node.

Link icon 🎨 Hexagons

To show how to make child components with canvas, let's draw something more fancy ✨

A single hexagon is defined with the folowing data:

A position on the screen — two x and y number values.

and number values. A radius to represent its size.

to represent its size. A rotation so that all hexagons don't look aligned.

so that all hexagons don't look aligned. A color .

We need a function that is able to generate some random hexagons. The randomisation code is not relevant here; let's just assume we have a way to get an array of hexagons. As for how to draw the shape of an hexagon – we need to draw a line between all its corners and then fill it with a color:

const corners = getHexagonCorners ( x , y , radius , rotation ) ; context . beginPath ( ) ; corners . forEach ( ( corner , index ) => { if ( index === 0 ) { context . moveTo ( corner . x , corner . y ) ; } else { context . lineTo ( corner . x , corner . y ) ; } } ) ; context . fillStyle = color ; context . fill ( ) ;

How could we extract this logic into its own Hexagon component? The component would need the canvas's context in order to draw anything. This could be passed via a prop to all child components, but this approach can become tedious when children are deeply nested. Another way of doing this is by using a React context to share "global" values between components.

Link icon 📦 A context in a context

At this point the naming gets a bit tricky as we are trying to share a canvas' context via a React context. Once we create a React context, we need to use the context's Provider to share a value. In the case of our Canvas component it would look like this:

const SharingContext = React . createContext ( null ) ; const Canvas = ( props ) => { const canvasRef = React . useRef ( null ) ; const [ renderingContext , setRenderingContext , ] = React . useState ( null ) ; React . useEffect ( ( ) => { const context2d = canvasRef . current . getContext ( "2d" ) ; setRenderingContext ( context2d ) ; } , [ ] ) ; return ( < SharingContext.Provider value = { renderingContext } > < canvas ref = { canvasRef } /> { } { props . children } </ SharingContext.Provider > ) ; } ; CodeSandbox icon Edit on CodeSandbox

The Hexagon component needs to consume this React context to read its value – here with the useContext hook.

const Hexagon = ( props ) => { const renderingContext = React . useContext ( SharingContext ) ; if ( renderingContext !== null ) { } } ; CodeSandbox icon Edit on CodeSandbox

Now that both our Canvas and Hexagon components are ready we are able to display randomly-generated hexagons:

const App = ( ) => ( < Canvas > { getRandomHexagons ( ) . map ( ( hexagon ) => ( < Hexagon { ... hexagon } /> ) ) } </ Canvas > ) ; CodeSandbox icon Edit on CodeSandbox

The last thing we need is to animate the hexagons so that they rotate.

Link icon 🎬 Animations

As we saw, the canvas is like a sheet of paper – once it's been drawn on, it can't be changed! However, a canvas can be cleared in order that something new can be draw on it. In that respect animating a canvas is somewhat like old-fashioned cartoon animation - we draw, clean, draw, clean and repeat until we achieve the desired effect. To make a shape move, you need to split the movement into small steps, draw them one by one, while clearing the canvas in-between. Those steps are called frames. Browsers come with an API requestAnimationFrame so that you can draw in each frame.

Link icon 🖼 Creating a frame loop

First things first - the canvas should be cleared at the beginning of each frame. The easiest way to do this is to have an internal state counting the frames. This way, the component re-renders at each frame:

const [ frameCount , setFrameCount ] = React . useState ( 0 ) ; React . useEffect ( ( ) => { const frameId = requestAnimationFrame ( ( ) => { setFrameCount ( frameCount + 1 ) ; } ) ; return ( ) => { cancelAnimationFrame ( frameId ) ; } ; } , [ frameCount , setFrameCount ] ) ; if ( context !== null ) { context . clearRect ( 0 , 0 , actualWidth , actualHeight ) ; } CodeSandbox icon Edit on CodeSandbox

But... the canvas is now white! This is because the hexagons are only rendered once - when <RandomHexagons/> is first rendered. But, as the canvas is cleared on each frame, the hexagons are erased once the next render occurs. Child components must be forced to re-render and draw in the canvas on every frame. One solution is to share the frameCount from the canvas with the Hexagon component. This is achieved via a React context, like we did with SharingContext :

const FrameContext = React . createContext ( 0 ) ; const Canvas = ( props ) => { return ( < SharingContext.Provider value = { renderingContext } > < FrameContext.Provider value = { frameCount } > < canvas /> { props . children } </ FrameContext.Provider > </ SharingContext.Provider > ) ; } ;

const Hexagon = ( props ) => { const renderingContext = React . useContext ( SharingContext ) ; const frameCount = React . useContext ( FrameContext ) ; } ;

And now our hexagons are back on the screen! 🎉 While this method works, the FrameContext has to be added to every child component. Two options are available to make sure that this context is used everywhere that SharingContext is:

We can regroup them into a single context that shares both the renderingContext and the frameCount . But the frameCount variable is not used in the child components, so it does not make sense to share its value.

Or, we can create a useCanvas hook to hide this complexity away! Even when consuming both React contexts, the hook can only return the canvas rendering context to the child components:

export const useCanvas = ( ) => { React . useContext ( FrameContext ) ; const renderingContext = React . useContext ( CanvasContext ) ; return renderingContext ; } ;

The Hexagon component logic now needs a small update to use this new hook:

const Hexagon = ( props ) => { const renderingContext = useCanvas ( ) ; } ;

Link icon 🔄 Making the hexagons move

If we want the hexagons to rotate, each hexagon should change its rotation angle at every frame - by incrementing it by 1, for example. To do this, the hexagons needs to remember the rotation from the previous render. We will use a React ref to achieve this:

const animatedRotation = React . useRef ( props . rotation ) ; animatedRotation . current = animatedRotation . current + 1 ;

As the Hexagon component re-renders at every frame, this code makes its rotation change at every frame: they rotate! 😍

Like we did with useCanvas , we can also improve the readability of this code by hiding the implementation details – here, using a React ref – in a hook:

const useAnimation = ( initialValue , valueUpdater ) => { const animatedValue = React . useRef ( initialValue ) ; animatedValue . current = valueUpdater ( animatedValue . current ) ; return animatedValue . current ; } ; CodeSandbox icon Edit on CodeSandbox

The Hexagon code now looks a bit better:

const Hexagon = ( props ) => { const animatedRotation = useAnimation ( props . rotation , ( angle ) => angle + 1 ) ; } ; CodeSandbox icon Edit on CodeSandbox

👏 Congratulations 👏 We now have animated canvas-based React components! We even created two custom hooks along the way to make our code nicer.

Link icon 👀 Going further