Exporting an embeddable React component from a Gatsby app using Webpack
A recent project called for the header and footer—implemented as React components in Gatsby—to be embedded on third-party sites (think support documentation). Since both components rely on Gatsby’s Link component as well as its GraphQL-backed Static Query (useStaticQuery in this case), some creativity was required. 
The solution: a separate Webpack configuration that would import the relevant file (e.g. the Header component) into a lightweight wrapper that included an explicit call to ReactDOM.render. To avoid a direct dependency on Gatsby, Webpack’s resolve directive was used to direct imports from Gatsby to a shim. 
Here’s the Header component that needs to be embedded. Note the multiple imports from Gatsby:
// src/components/Header.jsx
import React from "react"
import { Link, useStaticQuery, graphql } from "gatsby"
const query = `…` // query fetching link data from CMS
const Header = () => {
  const { navigation } = useStaticQuery(query)
  return (
    <div>Markup here making use of navigation data.</div>
  )
}
This is the embedded widget’s index.js where we import the Header component and render it to a predetermined element on the page:
import React from "react"
import ReactDOM from "react-dom"
import Header from "components/Header"
const baseStyles = {
  fontFamily: "Helvetica, Arial, sans-serif",
  lineHeight: 1.4,
  background: theme.white,
  color: "#444",
  fontSize: 16,
  boxSizing: "border-box",
  [["& *", "& *:before", "& *:after"]]: { boxSizing: "inherit" },
}
const HeaderEmbed = () => (
  <div css={baseStyles}>
    <Header />
  </div>
)
const headerTarget = document.querySelector("[data-render='embed-header']")
if (headerTarget) {
  ReactDOM.render(<HeaderEmbed />, headerTarget)
}
Getting Data from Gatsby into the Embed Component #
Gatsby renders out the result of static queries to JSON files at build time. These files each live at public/page-data/sq/d/${queryHash}.json, where the queryHash is a murmurhash of the query text itself using a seed of “abc”. This same hash is used to retrieve the data client-side when useStaticQuery is called (Gatsby replaces the query string itself with the hash).
# hash-query.js
const {
  stripIgnoredCharacters,
} = require("graphql/utilities/stripIgnoredCharacters")
const { murmurhash } = require("babel-plugin-remove-graphql-queries/murmur")
const GATSBY_HASH_SEED = "abc"
// This must match Gatsby's internal hashing
// https://github.com/gatsbyjs/gatsby/blob/eafb8c6e346188f27cf1687d26544c582ed5952a/packages/babel-plugin-remove-graphql-queries/src/index.js#L144
function hashQuery(query) {
  return murmurhash(stripIgnoredCharacters(query), GATSBY_HASH_SEED).toString()
}
module.exports = hashQuery
To dynamically import this JSON file into the embed, the query needs to be extracted from the src/components/Header.jsx file much the same way Gatsby does. Since this usage is narrower, the solution can take a shortcut from the full static analysis that Gatsby performs and use a simple regular expression to match and extract the query. This is all performed in the extractGraphQLQuery function in the below file. 
With the query text determined, it’s just a matter of computing the queryHash, reading the JSON file, and writing that data to a non-dynamic path that can be provided to the Header component when it calls useStaticQuery. This script (prepare-imports.js) will be run as a prebuild step for our embed, which is built after Gatsby has finished building:
// embed/prepare-imports.js
const fs = require("fs")
const path = require("path")
const hashQuery = require("./hash-query")
// Quick and rough routine to extract a single
// graphql tagged template literal from a JS file
function extractGraphQLQuery(sourcePath) {
  const data = fs.readFileSync(
    path.resolve(__dirname, "..", sourcePath),
    "utf8"
  )
  const match = /graphql`([^`]+)`/.exec(data)
  if (match) {
    return match[1]
  }
}
// Copy a cached graphql query result from `public`
// to the embed plugin folder and rename to ensure
// stable imports.
function prepareQueryForImport(
  name,
  sourcePath,
  targetFilename = `${name}.json`
) {
  const query = extractGraphQLQuery(sourcePath)
  const hash = hashQuery(query)
  const source = path.resolve(
    __dirname,
    "..",
    "public",
    "page-data",
    "sq",
    "d",
    `${hash}.json`
  )
  const dest = path.resolve(__dirname, "data", targetFilename)
  fs.copyFileSync(source, dest)
  return { name, hash, dest }
}
const queries = [
  prepareQueryForImport("header", "src/components/Header.js"),
  // Additional files can be added here as needed
]
// Export query data including hashes for reference in gatsby-shim.js
fs.writeFileSync(
  path.resolve(__dirname, "data", "queries.json"),
  JSON.stringify(queries)
)
This builds a file at embed/data/queries.json that looks like this:
[{
  "name": "header",
  "hash": "1287877988",
  "dest": "/absolute/path/to/example-project/embed/data/header.json"
}]
This queries.json dictionary can then be used to substitute in the correct data in the shimmed Gatsby imports:
// embed/gatsby-shim.js
// This file acts as a shim for components of Gatsby that
// are in use in shared components but need to be used outside
// of Gatsby in the embed.
import React from "react"
import hashQuery from "./hash-query"
import queries from "./data/queries.json"
// Import each query data file (as referenced in queries.json)
import headerData from "./data/header.json"
// Build a reference map between the query name (e.g. header)
// and the imported data for it.
const queryData = {
  header: headerData,
}
// Since Gatsby isn't being run, the query data actually
// gets passed to the `graphql` function, which allows us
// to compute its hash similar to how Gatsby does. The
// return value ends up being passed to `useStaticQuery`.
export const graphql = query => hashQuery(query.raw[0])
// We're using the query hash to determine which cached
// result data to return. This needs to match the return
// value from the native Gatsby function for compatibility.
export const useStaticQuery = queryHash => {
  const query = queries.find(query => query.hash === queryHash)
  if (!query) {
    throw new Error(`No matching query for ${queryHash}`)
  }
  return queryData[query.name].data
}
export const Link = ({
  activeClassName,
  activeStyle,
  getProps,
  innerRef,
  partiallyActive,
  ref,
  replace,
  to,
  ...rest
}) =>
  React.createElement("a", {
    ...rest,
    href: to,
  })
export default {}
With all of that established, Webpack can be configured with the following goals:
- Import the embed/index.jsembed wrapper and output atdist/widget.js
- Resolve imports from gatsbyto thegatsby-shim.jsfile
- Make React and ReactDOM externals (third-party website should have them available as globals)
- Use a Gatsby compatible Babel configuration
This will vary somewhat for each application, but for reference this is the webpack.config.js used in this solution:
const path = require("path")
const Dotenv = require("dotenv-webpack")
module.exports = {
  mode: "production",
  entry: "./embed/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "widget.js",
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            plugins: ["@babel/plugin-proposal-optional-chaining"],
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
              "@emotion/babel-preset-css-prop",
            ],
          },
        },
      },
      {
        test: /\.svg$/,
        use: ["@svgr/webpack", "url-loader"],
      },
    ],
  },
  resolve: {
    modules: [path.resolve(__dirname, "../src"), "node_modules"],
    extensions: [".js", ".jsx", ".json"],
    alias: {
      gatsby: path.resolve(__dirname, "gatsby-shim.js"),
    },
  },
  externals: {
    react: "React",
    "react-dom": "ReactDOM",
  },
  plugins: [
    new Dotenv({
      systemvars: true,
    }),
  ],
  optimization: {
    usedExports: true,
  },
  devtool: "nosources-source-map",
}
Tying it all together #
The final piece of the puzzle is orchestrating these pieces at build time. To do that, the scripts are configured as below in package.json:
{
  "scripts": {
    "prebuild:embed": "node embed/prepare-imports.js",
    "build:embed": "webpack --config embed/webpack.config.js",
    "postbuild:embed": "cp -R embed/dist/widget.* ./public/embed/",
    "build": "npm-run-all build:gatsby build:embed",
    "build:gatsby": "gatsby build",
  }
}
Since Yarn will automatically run prebuild: and postbuild: prefixed scripts, this recipe results in the following:
- gatsby buildis executed like normal
- prebuild:embedis run, executing the- prepare-importsscript that makes the GraphQL query data available to the embed at a consistent locaiton
- build:embedinvokes the isolated Webpack bundling of the embed itself
- postbuild:embedcopies the output from the embed build into the Gatsby- publicfolder for consumption by other applications
Here’s a simple example of how this embed gets used by downstream applications:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Header Embed Widget</title>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
    <!-- Header will be rendered inside this container -->
    <div data-render="embed-header"></div>
    <!--
      React and ReactDOM are dependencies; these tags will ensure
      they're available on the page, but they can be removed if compatible
      versions are already available.
    -->
    <script
      src="https://unpkg.com/react@16/umd/react.production.min.js"
      crossorigin
    ></script>
    <script
      src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"
      crossorigin
    ></script>
    <!-- This loads the embed code with all other dependencies and content -->
    <script
      src="/embed/widget.js"
      defer
      crossorigin
    ></script>
  </body>
</html>
Conclusion #
It’s not as cut and dry as a lot of the tasks React and Gatsby make easy for developers, but this solution has made it possible to reuse a fully featured Gatsby-dependent React component outside of the application without requiring any additional tooling for those implementing it.