Create Dead-Simple Canvas Animations in React

The canvas is a powerful element that can draw graphics, images, animations, and more. While it has been part of the HTML specification for years, I hadn’t realized its power until recently. The APIs can be clunky at first, especially when coming from a functional or reactive background, but are relatively easy to pick up. Here, I’ll walk through setting up animations with React and canvas.

The first example won’t be the most performant use of canvas, since it’s dependent on the React render cycle, but it’s the easiest to understand. The second example will use recursion, but it will take the burden off of the render cycle. The following code will be available on GitHub.

Option One: Local State

To get started, add a canvas element to your app. Make sure to create a ref for easy access, and specify a width and height.

import React, { useRef } from "react";

export const OptionOne: React.FC = () => {
  const canvas = useRef<HTMLCanvasElement | null>(null);
  return (
    <div>
      <canvas ref={canvas} width={960} height={640} />
    </div>
  );
};

With the canvas in place, we can reference it and start drawing to it. We’ll load an image with a ref, and draw to the canvas when it’s complete.

We can define a drawing function, like so:

export const OptionOne: React.FC = () => {
  // ...
  const logoRef = useRef<HTMLImageElement | null>(null);
  // ...
  useEffect(() => {
    // Load the images when the page mounts. Because we're using a ref,
    // they don't need to be in the dependency array or trigger a re-render

    const logo = new Image();
    logo.src = logoPath;
    logo.onload = () => {
      logoRef.current = logo;
      draw();
    };
  }, []);

  const draw = () => {
    const ctx = canvas?.current?.getContext("2d");
    const logo = logoRef.current;
    if (!ctx || !logo) {
      return;
    }

    ctx.drawImage(logo, 0, 0, logo.width, logo.height);
  };
  // ...
};

This should draw when the page loads, but because we want to animate, we need something else that we can change from an interval. Let’s follow in create-react-app’s footsteps. We’ll rotate the React logo around, but this time with a canvas instead of CSS. We can set up an interval to change a local state variable, and we can watch that for changes in a useEffect where we can draw to the canvas.

const radian = (n: number) => n * (Math.PI / 180);

const draw = (args) => {
  // ...
};

export const OptionOne: React.FC = () => {
  // ...
  const [rotation, setRotation] = useState(0);

  useEffect(() => {
    // ...

    // Set up an interval to trigger every 20ms to increase the rotation by one degree
    const interval: NodeJS.Timeout = setInteset rotationt; {
      setRotation((r) => r + radian(1)); // Angle in radians
    }, 20);
    return () => {
      clearInterval(interval);
    };
  }, []);

  useEffect(() => {
    draw({ canvas, logoRef, rotation });
  }, [rotation]);

  // ...
};

So now, every 20 milliseconds, we’ll increase the rotation by one degree. Then, the useEffect that’s watching the rotation value will be triggered, and the canvas will redraw. Note that we do not need to include canvas or logoRef in the dependency array here, because refs are mutable and they do not trigger a component re-render.

For the purposes of this post, I’ll leave the contents of the draw function out, but the full code is available on GitHub.

Pros & Cons of This Option

With that, we should have a rotating image of the React logo! I believe that this is the easiest model to understand canvas animations in React – as a value changes, we react to the changes and draw onto a canvas. This is my preferred approach when the animations are simple, and I want to keep the same mental model as React. I find it easier to control timing with the setInterval function with this approach as well, if the animation doesn’t need constant updating.

However, this option is not necessarily the most performant way to use a canvas. The next option puts the burden of redrawing onto the browser and outside of React, but the recursive nature can be more difficult to understand. Additionally, this relies on mutability, as it uses React refs more heavily.

Option Two: Recursion

In this example, we’ll use the same setup code to create a canvas and load an image. The difference primarily lies with setting up the state management for the rotation and kicking off the animation. In this case, we’ll need to use the requestAnimationFrame method on the window object. Additionally, we’ll increase the rotationRef before calling the animate function recursively.

export const OptionTwo: React.FC = () => {
  // ...
  const rotationRef = useRef(0);

  const animate = () => {
    // When the component is unmounted, we will lose the canvas.current ref, so exit early to clean up
    if (!canvas.current) {
      return;
    }
    const rotation = rotationRef.current;
    draw({ canvas, logoRef, rotation });
    rotationRef.current = rotation + radian(1);

    requestAnimationFrame(animate);
  };

  useEffect(() => {
    // ...
    // this is where we would load the images

    // Start the animation function
    requestAnimationFrame(animate);
  }, []);

  // ...
};

And with that, we can animate a canvas in React! Make sure to check out the GitHub repo to see the full example.

You Can Create Dead-Simple Canvas Animations in React

We’ve covered two distinct ways to perform canvas animations in React. The first one is slightly easier to understand, as it has a similar mental model to general React development. I’ve found it to be easier to control the timing of animations and to respond to state changes. The second one is more performant because it relies on the browser to orchestrate the animation. I believe there’s room for both of these when animating a canvas in React, but I’m happy to be proven wrong in the comments below!

Conversation
  • Carlos says:

    Hey Dan,

    I’m commenting about a post you did in 2020 for chat.db and sqlite

    When I open it all my messages are there and I can see the deleted ones show no text.

    There’s a deleted messages folder but it’s empty (I think)

    Is there a way to open or find the Deleted messages from
    Chat.db in sqlite? Thank you

  • Comments are closed.