Be mindful of React and re-rendering

At 1stdibs, we’ve been migrating our Backbone-heavy front-end to React for several months. Different parts of our platform are being ported at different times, this leads to some teams being newer to React than others. Largely, we’ve been happy with how React has been performing on our high-traffic production site. Then, a doozy of a bug hit. One that had me, a relatively new React user, scratching my head.

We use a service called Rollbar to track JavaScript errors in the wild. When this bug hit production, we used our monthly allotted error tracking in under two days. It was out of control and needed to be addressed ASAP.

The error message itself wasn’t very useful. However, the error stack clearly pointed to React:

Error: Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.
1 File "webpack:///./~/react/~/fbjs/lib/invariant.js?dd1a" line 46 col 1 in invariant
throw error;
2 File "webpack:///./~/react/lib/ReactMount.js?65e8" line 764 col 1 in ReactMount.findComponentRoot
!false ? process.env.NODE_ENV !== 'production' ? invariant(false, 'findComponentRoot(..., %s)...
3 File "webpack:///./~/react/lib/ReactMount.js?65e8" line 679 col 1 in ReactMount.findReactNodeByID
return ReactMount.findComponentRoot(reactRoot, id);
4 File "webpack:///./~/react/lib/ReactMount.js?65e8" line 176 col 1 in getNodeFromInstance
if (ReactEmptyComponentRegistry.isNullComponentID(id)) {
5 File "webpack:///./~/react/lib/findDOMNode.js?8290" line 36 col 1 in findDOMNode
if (componentOrElement == null) {
// etc

Additionally, the Rollbar dashboard made it clear that the errors were coming exclusively from Internet Explorer 11.

Since the errors were coming from IE, and we’re an OS X shop, I spun up a Windows virtual machine to start debugging. Once I was able to view localhost running unminified React, I got this new error message:

Warning: ReactMount: Root element has been removed from its 
original container. New container: null 
findComponentRoot(..., 3) Unable to find element. This probably 
means the DOM was unexpectedly mutated (e.g., by the browser), 
usually due to forgetting a <tbody> when using tables, nesting 
tags like <form>, <p>, or <a>, or using non-SVG elements in an 
<svg> parent. Try inspecting the child nodes of the element 
with React ID ``.

Since I had no experience with this sort of issue in React, this error didn’t seem super-helpful initially (especially the blank React ID). In retrospect, it did point me in the right direction. The error ultimately had to do with React rendering components into a DOM element that was initially static markup not generated by React. For example, the markup looked similar to this:

<div class="parent-wrapper">
    <div class="target-element"></div>

It wasn’t a super-obvious bug at first. The element itself was just an empty div for content to be rendered into, which seemed pretty harmless.

However, doing some additional debugging and stepping through the code, there was one pattern that I kept running into. It looked similar to this:

// JavaScript to find a DOM element and 
// inject a React component into it

// [1] import react component
const component = require('./my-component');

// [2] get the target DOM element
const targetEl = document.querySelectorAll('.target-element')[0];

// [3] create the react element (with props {...})
const reactComp = React.createElement(component, {...});

// [4] render react component into the DOM target
ReactDOM.render(reactComp, targetEl);

Create a React component, add it to the DOM. Not a big deal.

However, since we’re in a transition phase, the way we’re handling the rendering in this particular circumstance isn’t optimal. The .parent-wrapper element containing .target-element was also the target of a ReactDOM.render call. When React renders into .parent-wrapper its reference to .target-element is wiped away. Its handle on it was lost. When it came time to re-render to .target-element, it had been removed from its original place in the DOM, as pointed out in the error message:

Warning: ReactMount: Root element has been removed from its 
original container.

It actually wasn’t some idiosyncratic issue with IE11, but a mistake in the way our own DOM structure was setup vis a vis React rendering. The strangest thing about this error was that it only occurred in IE11 and only with React 0.14.*.

I spent hours on this bug pulling my hair out (figuratively) and cussing up a storm (literally). The take away is to be very mindful of where you’re rendering your React components with respect to your static markup.