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.