Webpack, why thy so complicated? and How to Pass a React Component to your Gatsby Plugin options

Today I learned something new (again) by trying to answer a Stackoverflow question.

if you want a plug-in to take a component as an input (either as a function/class or as a string of a path to a module) ... how can you actually use that component in your plug-in?

Before we start, let's set up a simple Gatsby project structure:

root
  |-- <etc>
  |-- gatsby-config.js
  |-- my-component.js
  |-- plugins
       `-- my-custom-plugin
           |--gatsby-node.js
           |--gatsby-ssr.js
           |--gatsby-browser.js
           `--package.json

Gatsby config:

// gatsby-config.js
import 

module.exports = {
  plugins: [
    {
      resolve: 'my-custom-plugin',
      options: {
        componentPath: path.join(__dirname, './my-component.js')
      }
    }
  ]
}

Here's the full answer.

This seems like it should be a simple question

I had the same thought while trying this out myself. Oh boy.

TL;DR:

// gatsby-node.js
const { DefinePlugin } = require('webpack')
const path = require('path')

exports.onCreateWebpackConfig = ({ actions }, { componentPath }) => {
  actions.setWebpackConfig({
    plugins: [
      new DefinePlugin({
        '___COMPONENT___': JSON.stringify(componentPath)
      })
    ]
  })
}
// gatsby-ssr
export const onRenderBody = ({ setPreBodyComponents }) => {
  const Component = require(___COMPONENT___).default
  setPreBodyComponents([<Component />])
}

Long read

Gatsby config doesn't seem to pass functions around (I could have sworn it used to), so passing a React component directly to your custom plugin is out the window. It has to be a path to your component.

You didn't say if you're using the component in gatsby-node or gatsby-browser/ssr, but I assume it's the latter since requiring stuff dynamically in Node is dead simple:

Gatsby Node

// gatsby-node.js

function consume(component) {
  const Component = require(component)
}

Node doesn't understand JSX or ESM. However, that's a different problem.

Gatsby Browser

gatsby-browser/ssr is run with webpack, so the module format is not a problem. But import(componentPath) won't work:

Dynamic expressions in import()

It is not possible to use a fully dynamic import statement, such as import(foo). Because foo could potentially be any path to any file in your system or project.

webpack doc

Ok, I suppose so something like this should work:

// gatsby-browser
import('./my-dir' + componentPath)

Nope, because webpack will try to resolve this from wherever the plugin lives, i.e. node_modules or plugins directory & we're not about to ask our users to put their custom components in node_modules.

What about this, then?

// gatsby-browser
import(process.cwd() + componentPath) // nope

We're right back at the beginning — webpack doesn't like a full dynamic path! And even if this works, this is a terrible idea since webpack will try to bundle the whole working directory.


Only if we could encode the path as a static string beforehand, so webpack can just read that code — like using webpack.DefinePlugin to define environment variables. Fortunately we can do that in gatsby-node.js:

// gatsby-node.js
const { DefinePlugin } = require('webpack')
const path = require('path')

exports.onCreateWebpackConfig = ({ actions }) => {
  actions.setWebpackConfig({
    plugins: [
      new DefinePlugin({
        '___CURRENT_DIR___': JSON.stringify(process.cwd())
      })
    ]
  })
}

And finally

// gatsby-browser

// eslint throw error for unknown var, so disable it
// eslint-disable-next-line
import(___CURRENT_DIR___ + componentPath) // works, but don't do this

But since we can access user options right in gatsby-node, let's encode the whole path:

  // gatsby-node.js
  const { DefinePlugin } = require('webpack')
- const path = require('path')

- exports.onCreateWebpackConfig = ({ actions }) => {
+ exports.onCreateWebpackConfig = ({ actions }, { componentPath }) => {
    actions.setWebpackConfig({
      plugins: [
        new DefinePlugin({
-         '___CURRENT_DIR___': JSON.stringify(process.cwd())
+         '___COMPONENT___': JSON.stringify(componentPath)

        })
      ]
    })
  }

Back in gatsby-browser.js:

// gatsby-browser

// I pick a random API to test, can't imagine why one would import a module in this API
export const onRouteUpdate = async () => {
  // eslint-disable-next-line
  const { default: Component } = await import(___COMPONENT___)
  console.log(Component) // works
}

Gatsby SSR

For the sake of completeness, let's try the same trick in gatby-ssr:

// gatsby-ssr

export const onRenderBody = async ({ setPreBodyComponents }) => {
  // const Component = require(___COMPONENT___).default
  const { default: Component } = await import(___COMPONENT___)
  setPreBodyComponents([<Component />])
}

...and it failed.

Why? If one's curious enough, they might dig around Gatsby code to see how gatsby-ssr is treated differently from gatsby-browser, but alas, I don't feel like doing that.

Fear not, we still have one trick up our sleeve. Webpack's require can import module dynamically too, though not asynchronously. Since gatsby-ssr doesn't run in the browser, I couldn't care less about asynchronicity.

export const onRenderBody = ({ setPreBodyComponents }) => {
  const Component = require(___COMPONENT___).default
  setPreBodyComponents([<Component />]) // works
}

And now it works.

Sharing code between gatsby-ssr & gatsby-browser

Let's say we need this component in both gatsby-ssr and gatsby-browser — would require(...) works in gatsby-browser too?

export const onRouteUpdate = async () => {
  // eslint-disable-next-line
  const { default: Component } = require(___COMPONENT___)
  console.log(Component) // yes
}

It works.

import(..) vs require()

While import() does load stuff dynamically, it is more of a code-splitting tool. Here are some differences other than asynchronicity:

  • using import('./my-dir' + componentPath) will bundle all files inside ./my-dir into a chunk. There're magic comments we can use to exclude/include stuff.
  • require(...) will inline the required component into whatever chunk's calling it.