Technical: NCIDS Initialization in Gatsby - NCIOCPL/ncids GitHub Wiki

Need

We have a number of NCIDS Components that use Javascript to progressively enhance those components. When displaying the components on the Documentation site we need to call the initialization scripts for those components. Under normal circumstances this would not be a problem. However, the introduction of Gatsby (v3) and React, bring a fair set of issues.

Background

Gatsby's (v3) Rendering

While Gatsby is a static site generator, it does not exactly generate a purely static web site. It is in essence a Server Side Rendered (SSR) React app, which outputs the initial build up DOM to HTML, as well as a json file with the page context. When a browser requests the HTML, the page is loaded and a lighter-weight React app rehydrates the state on load using the HTML and the json. Gatsby/React can generate this static HTML as long as none of the React components used are built with any hooks that delay elements being added to the DOM (e.g., useEffect). For example, if the output of the layout template is done in a useEffect, with a loading spinner or something for the initial content, Gatsby would generate the loading spinner into the HTML. Ok, so now onto the other side of Gatsby and SSR. Gatsby is only SSR for the published site. When running the development server, a lot more of Gatsby (v3) runs in the browser. So something that might work in the browser will not work on the published site and vice-versa.

In-site page navigation

Continuing on this React app thread, Gatsby <Link>. While <Link> may render out an anchor tag, it will wire it up for pre-fetching if the destination is within the Gatsby site. What this means is that the page will be loaded up into the virtual DOM and the _gatsby element on the page will get its contents replaced with the contents of the link destination. In other words, the page will not load like a normal web page with normal events like DOMContentLoaded. This happens on the published site as well. (It does this to make pages zipper and ensure that a complete page is displayed.)

Script tags

Another important thing to note is that you can't just slap a <script> tag into some jsx and expect the browser to process it normally. One might think so because we are "rendering static HTML," but it does not work that way. The web page still has to be generated by React, and even using dangerouslySetInnerHTML still leads to a way where the script is not evaluated. (So yeah, in your static HTML page it would be ok, but development and weird pre-fetching it would fail.) There is a package to help with this, dangerously-set-html-content. However, it was not build for SSR, only client side. So it ends up using a combination of a ref and a useEffect. This means it will not show in the initial HTML of the page.

Component HTML

Our NCIDS Component HTML is not actually a React component. We are actually "live" previewing a Markdown HTML code block. This way the same HTML for syntax highlighting is used to display the component. This is as a "if you put this HTML on your page, this is what you will see" type experience. So in our case we are converting the HTML to JSX so that we can append it as a child. This does get rendered on the initial pass.

Solution

So given the multitude of ordering of script tags and elements actually getting added to the DOM we have something that seems to work right now.

Rough Set of Events

  1. [gatsby-browser.js] custom element <ncids-code-preview/> is registered
  2. [React] First Rendering
    1. [src/components/Code.jsx] String of HTML Code from markdown is turned into JSX
    2. [src/components/Code.jsx] JSX is added to preview custom element (<ncids-code-preview/>)
    3. [src/components/NciDsScriptInit] Raw script for handling NCIDS initialization is pulled from markdown and configured
    4. [src/components/NciDsScriptInit] ScriptWrapper is created with the raw script
      1. [src/components/ScriptWrapper.jsx] Empty <div> is added to virtual DOM
  3. [React] virtual DOM is sent to real DOM
    1. [gatsby-browser.js] <ncids-code-preview/> constructor is called. (This occurs for each tag on the page)
    2. [gatsby-browser.js] <ncids-code-preview/> connected callback is called
      1. [gatsby-browser.js] NCIDS:ShouldBeReady event listener is added to window.
  4. [React] Second Rendering
    1. (nothing changes with Code)
    2. [src/components/ScriptWrapper.js] Script tag with initialization from <NciDsScriptInit> is added to real DOM
      • I think this bypasses the virtual DOM, but for the purposes of this document, let's assume not.
  5. [React] virtual DOM is sent to the real DOM
    1. [src/components/ScriptWrapper.jsx] Script tag from 4.2 is evaluated by the browser
    2. [src/components/NciDsScriptInit] Script registers a NCIDS:Preview listener on the window.
    3. [src/components/NciDsScriptInit] Script dispatches a NCIDS:ShouldBeReady event to the window.
    4. [gatsby-browser.js] NCIDS:Preview event is dispatched on the custom <ncids-code-preview/> element. (Remember, each tag on the page is separate, so each one would fire.)
    5. [src/components/NciDsScriptInit] NCIDS:Preview event listener is executed. (for each <ncids-code-preview> on the page.
    6. [MDX File] The script contents of the <NciDsScriptInit> component on the MDX page is executed.
⚠️ **GitHub.com Fallback** ⚠️