Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gatsby Plug-in: How Can I Take a Component as a Plug-In Option?

I'm trying to make an improvement to an existing Gatsby plug-in, and I want to pass a React Component to the plug-in, through its configuration entry in gatsby-config.js:

  plugins: [
    {
      resolve: `gatsby-plugin-modal-routing`,
      options: { someComponent: SomeComponentClassOrFunction }
    },

However, the problem I'm running into is that I can't figure out how to make it work.

If I try to pass the component itself as part of the plug-in's configuration, it seems to get serialized to/from JSON, resulting in the class becoming a useless object. So it seems I have to pass a path string instead.

  plugins: [
    {
      resolve: `gatsby-plugin-modal-routing`,
      options: {
        modalComponentPath: path.join(__dirname, 'src/components/SomeComponent.js')
      }
    },

However, if I try to pass the path instead, I can't figure out how to use it to load the component inside the plug-in. I've tried using a dynamic Node import (ie. import(path).then(component => ...)) ...

  • with a path that's path.join-ed with __dirname
  • with a relative path (src/components/SomeComponent)
  • with a local-path-relative path (./src/components/SomeComponent)
  • with and without a trailing .js

I'm not sure if this is some sort of issue with the different paths of the app vs. the plug-in or whether there's some other problem, but using import seems like an un-Gatsby-like solution anyway.

So, then I discovered the loadPage and loadPageSync functions which are passed into the plug-in ... but those failed also. Every path I try results in component coming back ... but it's a "page not found" component (presumably because the component I'm trying to pass in hasn't been added as a page).

This seems like it should be a simple question, at least to anyone who has worked on Gatsby plug-ins before: 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?

All I'm looking for is a basic pattern or reference to a line in an existing Gatsby plugin that takes a component, or something simple like that (I can look up any details).

like image 648
machineghost Avatar asked Jul 18 '20 17:07

machineghost


People also ask

Can you use react components in Gatsby?

React's component architecture simplifies building large websites by encouraging modularity, reusability, and clear abstractions. React has a large ecosystem of open source components, tutorials, and tooling that can be used seamlessly for building sites with Gatsby.

What are plugins in Gatsby?

In Gatsby terms, a plugin is a separate npm package that you install to add extra features to your site. There are a variety of plugins that each have different use cases. Some plugins provide pre-built components, others add analytics, others let you pull data into your site.


1 Answers

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.

// gatsby-config.js

{
  resolve: 'my-custom-plugin',
  options: {
    componentPath: path.join(__dirname, './my-component.js')
  }
}

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

Gatsby Node

// gatsby-node.js

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

...although it doesn't understand JSX or ESM, but 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 live, 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 full dynamic path! And also 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 just 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 go and dig around Gatsby code to see how gatsby-ssr is treated differently than gatsby-browser, but alas I just 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 just 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 tool for code-splitting. Here's some different, other than asynchronicity:

  • using import('./my-dir' + componentPath) will bundle all files inside ./my-dir into a chunk. There's magic comment we can use to exclude/include stuff.

  • require(...) will just inline the required component into whatever chunk's calling it.

like image 81
Derek Nguyen Avatar answered Oct 31 '22 11:10

Derek Nguyen