How to Prevent a Duplicated Canvas When Using P5 and React Strict Mode

@lloydjatkinson

When using P5 in combination with React strict mode almost immediately you’ll find yourself facing the surprising problem of two canvases being rendered. When searching for an explanation I came across an accepted Stack Overflow answer that said “turn off strict mode”. Hardly a good fix.

I’ll explain why this happens and how to fix it. Firstly, consider this minimal reproducible example.

import { useEffect, useId } from 'react';
import P5 from 'p5';

const visualisation = ({ width, height }: { width, height }) => {
	const sketch = (p5: P5) => {
		p5.setup = () => {
			p5.createCanvas(width, height);
		};
		p5.draw = () => {
			p5.line(0, 0, width, height);
		};
	};

	const p5 = new P5(sketch);
};

export const BrokenDemonstration = ({ width, height }: { width: number, height: number }) => {
	const id = useId();

	useEffect(() => {
		visualisation({
    		width,
			height,
    	});

	}, []);

	return <div id={id}></div>;
};

Creating the P5 instance is considered a side effect so useEffect is used. P5 then proceeds to render a canvas. So far, so good. But as we know, strict mode will call hooks including useEffect and useState twice. What does this mean for our P5 instance? Two will be created and you should see two lines instead of one.

Unfortunately, it can be difficult to detect these problems as they can often be non-deterministic. Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions…

To fix this we will use the remove method described in the the P5 docs, this sounds like the correct solution we need! A lot better than the “turn off strict mode” suggestion.

Removes the entire p5 sketch. This will remove the canvas and any elements created by p5.js. It will also stop the draw loop and unbind any properties or methods from the window global scope.

So we will call this method in useEffect. This is also a great opportunity to create a type for this. I have this in it’s own module so it can be reused but I’ve shown it inline for the sake of this post.

import { useEffect, useId } from 'react';
import P5 from 'p5';

type SketchCleanup = { cleanup: () => void };

const visualisation = ({ width, height }: { width: number, height: number }): SketchCleanup => {
	const sketch = (p5: P5) => {
		p5.setup = () => {
			p5.createCanvas(width, height);
		};
		p5.draw = () => {
			p5.line(0, 0, width, height);
		};
	};

	const p5 = new P5(sketch);

	return {
		cleanup: p5.remove,
	};
};

export const WorkingDemonstration = ({ width, height }: { width: number, height: number }) => {
	const id = useId();

	useEffect(() => {
		const { cleanup } = visualisation({
	    	width,
			height,
		});

		return cleanup; // This removes the canvas when the component is rerendered.
  	}, []);

	return <div id={id}></div>;
};

Problem solved! Now we don’t have to sacrifice the benefits of strict mode when using P5 with React.

Share:

Need help with your software project? Let's talk

Stay up to date

Subscribe to my newsletter to stay up to date on my articles and projects

Support me on Kofi