Skip to content

Instantly share code, notes, and snippets.

@kimniche
Last active November 27, 2017 14:21
Show Gist options
  • Save kimniche/4829022685ed1ffa7659c491e6e8c978 to your computer and use it in GitHub Desktop.
Save kimniche/4829022685ed1ffa7659c491e6e8c978 to your computer and use it in GitHub Desktop.

Gotchas

Lingering instances of React.PropTypes

Solution: swap to PropTypes

Dependencies

  • LazyInput
    • problem: PropTypes, React.createClass
    • doesn't appear to be maintained
    • single file, Website repo only
    • our solution: port file into Website repo
  • @mapbox/range
    • problem: PropTypes
    • single file, Website repo only
    • our solution: port file into Website repo
  • Helmet updated from v3.1.0 to v5.0.0
    • problem: PropTypes (despite source using them properly..)
    • our solution: we don't really need this/can roll our own via React 16 Portals (more details below)
  • Enzyme  * problem: our version is incompatible with React 16  * solution: update from v2.9.1 to 3.2.0
    • install enzyme-adapter-react-16
    • simplify mocha npm scripts with mocha.opts file
    • configure adapter (like we do for EA)
  • react-addons-css-transition-group

react-with-addons.js build defunct, different names for CDN builds of react and react-dom

Our code before:

// Html.jsx

<script src={ `https://cdnjs.cloudflare.com/ajax/libs/react/${cdnFileVersion('react', 'react-with-addons')}` }></script>
<script src={ `https://cdnjs.cloudflare.com/ajax/libs/react/${cdnFileVersion('react-dom')}` }></script>

And after:

// Html.jsx

<script crossOrigin="true" src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossOrigin="true" src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

Swapping out react-helmet for Portals (Metadata)

The gist

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

With Portals, we can attach components anywhere (via document.querySelector) in the DOM. Typical use case: modals/dialogs, tooltips -- anywhere a child element may need to break out of a parent container

How do portals help us?

Currently, we use react-helmet to let us sneak metadata into the proper location in the DOM (in head) from different components. React Portals let us accomplish the same idea.

Complications

Portals work by attaching to a DOM -- meaning we're SoL when it comes to SSR. So we got creative; the same way we track which code-split chunks are used in each render, we track what portals are called, and "flush" them into the DOM during server render.

This article inspired our approach, but is a little magical: https://michalzalecki.com/render-react-portals-on-the-server/ - but to keep things simple, we're not using any third party packages.

// render.js

// Gather chunks that were sync loaded on the server
// so we can flush proper initial chunks to client
let chunkNames = []
const pushChunkName = chunkName => {
    chunkNames = [ ...chunkNames, chunkName ]
}

let portals = []
const addPortalSSR = (children, selector) => {
    portals = [ ...portals, { children, selector }]
}

<AppRoutes
    pushChunkName={ pushChunkName }
    addPortalSSR={ addPortalSSR }
    />

exposes both pushChunkName and addPortalSSR as functions in child context, so any server-rendered component can tap into the methods.

const Head = ({ children }, { addPortalSSR }) => {
    if (addPortalSSR) {
        addPortalSSR(children, 'head')
        return null
    }

    return ReactDOM.createPortal(children, document.querySelector('head'))
}

Now we have the portals stashed in our portals variable by the time we get to rendering the markup to string:

let html = renderToString(
    <HtmlComponent
        context={ componentContext }
        state={ exposedState }
        markup={ motherLoad }
        chunkNames={ chunkNames }
    />,
)

Given our rendered-to-string markup, now all we have to do now is inject the portals in the appropriate containers before sending the markup to the client:

html = _appendPortalsToMarkup(portals, html) // uses cheero library for server-side DOM manipulation, similar to jQuery
res.status(200).send(`<!DOCTYPE html>${html}`)

Test Changes

  • Adapter (like with EA)
  • expect is now global variable (like with EA)
  • Test helpers file moved to setupMocha.js

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment