FBRY

Setting Up Processing + Gatsby + React

One of the things I wanted to do with this site is use the awesome Processing for JavaScript library p5.js. p5.js is a pretty sweet library, but it’s not designed to be used with React – at least, not easily.

For this project, we'll be trying to get the following MDX file working properly:

---
title: "Basic Sketch"
date: "2020-04-11"
---

import "p5"

<Sketch
    setup={(p, canvasParentRef) => {
        p.createCanvas(640,480).parent(canvasParentRef)
        p.noLoop()
    }}

    draw={(p) => {
        p.cirlce(p.width/2, p.height/2, 80)
    }}
/>

Problem 1: Getting p5.js Into React

p5.js is meant to be delivered via CDN to a website, not used as an NPM package. This doesn’t mean you can’t, there is an official p5.js NPM package. However, using p5.js this way is not as simple or elegant as using it the usual way. The biggest difference is that you must create a p5 object that contains all your sketch’s functions and link that object to a canvas element.

While trivial to accomplish in React, I found a handy React component that already put in the leg work: react-p5. This package provides a Sketch component that allowed me to provide setup(), draw(), and all the other usual p5.js sketch functions as props. The Sketch component then handles all the management of p5.js.

Problem 2: Not Breaking the Gatsby Build

Sweet! We’ve got p5.js into our Gatsby React websit- Fuck, the build’s broken.

Ain’t that the way. Well, what exactly is the problem? Gatsby complains about there being no variable window defined:

ERROR 

There was an error compiling the html.js component for the development server.

See our docs page on debugging HTML builds for help https://gatsby.dev/debug-html ReferenceError: window is not defined

  69915 |           // requestAnimationFrame polyfill by Erik Möller
  69916 |           // fixes from Paul Irish and Tino Zijdel        
> 69917 |           window.requestAnimationFrame = (function() {    
        | ^
  69918 |             return (
  69919 |               window.requestAnimationFrame ||
  69920 |               window.webkitRequestAnimationFrame ||

  WebpackError: ReferenceError: window is not defined

For those just learning Gatsby, like myself at the time of writing this, that’s a little confusing. Why should Gatsby care that window isn’t defined? Isn’t it just rendering our components as HTML?

Yes, yes, it is rendering our components to HTML. And the problem is found there: Gatsby uses native React functions to render HTML components outside of the browser environment, much like how test renderers work. When Gatsby does this, it makes a very big assumption: your components don’t use browser defined variables.

Now this assumption isn’t without reason. If your pages are supposed to be statically rendered, they shouldn’t use browser defined variables when being rendered. It defeats the whole purpose of being statically rendered if they depend on the client.

But, obviously, we sometimes want to use browser variables. This means we have to trick Gatsby into not rendering our components that violate its core assumption.

Enter: The Loadable Component

Turns out that this is such a common problem in the React ecosystem that there’s a library out there for it. It’s called @loadable/component, and it provides a decorator for creating loadable components.

On the tin, @loadable/component states that it provides “React code splitting made easy.” Being new to React, that makes zero sense. But after some research, I found that code splitting is the practice of literally splitting your code into separate bundles. @loadable/component does this by providing a decorator that accepts a function that returns the result of a dynamic import as its parameter.

This decorator generates a wrapper component that renders the component that the dynamic import returns. This all assumes that the component you want to render is the default export of the module you are loading. If it’s not, this whole thing breaks. Luckily for us, React standard practice dictates that the component you want people to render is always the default export, and the author of react-p5 follows that standard practice.

This allows for some, almost deceptively, simple code:

import loadable from "@loadable/component"

const Sketch = loadable(() => import("react-p5"))

The component created can be passed into MDXProvider component as a shortcode that can be used by the gatsby-plugin-mdx plugin:

const shortcodes = { Sketch }

const Template = () => (
    <MDXProvider shortcodes={shortcodes}>
        ...
    </MDXProvider>
)

Why this saves our bacon is because Gatsby, under the hood, uses Webpack to bundle the client-side code. Webpack automatically registers that when it sees a dynamic import, the code that is imported needs to be isolated into its own bundle that will be loaded by the client during runtime. And because dynamic imports can only happen during runtime, Gatsby’s React renderer ignores them when performing static rendering, preventing it from trying to execute code that needs to be run in the browser.

"Your Solution is Trash, My Build is Still Broke"

If you checked your Gatsby build, you probably noticed that it’s still broke. And you’ll also have noticed that Gatsby is still complaining about the window variable not being defined. So, sounds like code splitting didn’t save us at all.

But this is a different error. Our loadable component can’t save us from needing to include p5.js in our sketch file, which can’t be lazily loaded since our sketch file's code get's executed outside of our area of influence. For this we need a different trick: null loading client-only modules.

This is a trick that comes from Webpack. To implement it, we have to edit (or in some cases, create) a gatsby-node.js file that contains the following snippet:

exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
    if (stage === "build-html" || stage === "develop-html") {
        actions.setWebpackConfig({
            module: {
                rules: [
                    {
                        test: /p5/,
                        use: loaders.null()
                    }
                ]
            }
        })
    }
}

What this snippet does is modify the generated Webpack configuration made by Gatsby’s build routine with a module rule that states: If a package contains the string “p5”, load it with the null-loader. This null-loader passes a null module to Webpack, preventing it from trying to bundle the module and causing a build error.

If you restart the Gatsby development server and watch the initial build, it will succeed without so much as a peep! (Assuming the rest of your build isn’t causing grief!)