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 => ...)
) ...
path.join
-ed with __dirname
src/components/SomeComponent
)./src/components/SomeComponent
).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).
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.
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.
This seems like it should be a simple question
I had the same thought while trying this out myself. Oh boy.
// 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 />])
}
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.js
function consume(component) {
const Component = require(component)
}
...although it doesn't understand JSX or ESM, but that's a different problem.
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
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With