p5.js and SSR

Thought I would share some of my experience getting p5 to run in an SSR(server-side rendering) environnment. I would like to explain SSR in a separate blog post, but for now, you might want to do this because you build with React and want to render parts of website on the server, but also want to include a p5.js sketch.

The issue is that p5.js references the ‘window’ object on import, so any attempt to include the library, even if the p5 module is not utilized on the server side, fails. Below is a bare bones minimum component definition that has worked for me.

First, the easy part. A sketch.ts file that contains setup and draw functions just like how you might expect a p5 sketch to have. The init function returns a closure which allows React to tell p5 to cleanup the sketch.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// sketch.ts
import p5 from 'p5';

export function sketch(p5: p5) {
  let x = 100;
  let y = 100;

  p5.setup = function() {
    p5.createCanvas(700, 610);
  };

  p5.draw = function() {
    p5.background(0);
    p5.fill(255);
    p5.rect(x, y, 50, 50);
  };
}

export function init(root: HTMLElement): () => void {
  let instance = new p5(sketch, root);
  return () => {
    instance.remove()
  }
}

And here are the most important bits:

  1. The React component uses useRef so that we can locate an HTML node for p5 to attach to. I think some version of this is necessary, otherwise p5 cannot get access to the DOM.
  2. A useEffect function ensures that the p5 script is only loaded once, and only loaded client side.
  3. We dynamiclly import the sketch file(and therefore the p5 library) only on the client side. Notice that there are no references to the p5 library in this file, and so it can be safely compiled and run on the server side.
  4. We ensure that we assign and call a tearDown function, so that when React cleans this component up, the sketch goes away as well
  5. Note the check for the ‘active’ boolean. The component may get unmounted before the dynamic import resolves, and if this check is not in place, p5 will still load and run.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Page.tsx
const Page = () => {
  const p5ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let active = true
    let tearDown = function() { }

    import("./sketch").then((s) => {
      if (!active) { return }
      tearDown = s.init(p5ref.current!)
    })

    return () => {
      active = false
      tearDown()
    }
  }, [])

  return (
    <div ref={p5ref} id="p5" className=''>
    </div>
  )
}