Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Diagnosing RangeError: Maximum call stack size exceeded in React KeyEscapeUtils

Background

Our webapp is written with React and Redux using the official react-redux bindings. Another primary library used in this web app is PaperJS. We recently transitioned this to being a Redux app, though it has used React for a while.

The Problem

Refreshing sometimes (usually every other refresh) causes a

RangeError: Maximum call stack size exceeded
at String.replace (<anonymous>)
at Object.unescape (KeyEscapeUtils.js:49)
at flattenSingleChildIntoContext (flattenChildren.js:32)
at flattenChildren.js:53
at traverseAllChildrenImpl (traverseAllChildren.js:69)
at traverseAllChildrenImpl (traverseAllChildren.js:85)
at traverseAllChildren (traverseAllChildren.js:157)
at flattenChildren (flattenChildren.js:52)
at ReactDOMComponent._reconcilerUpdateChildren (ReactMultiChild.js:209)
at ReactDOMComponent._updateChildren (ReactMultiChild.js:315)

Here's the React source code where it's failing:

return ('' + keySubstring).replace(unescapeRegex, function (match) {
  return unescaperLookup[match];
});

and in context:

/**
 * Unescape and unwrap key for human-readable display
 *
 * @param {string} key to unescape.
 * @return {string} the unescaped key.
 */
function unescape(key) {
  var unescapeRegex = /(=0|=2)/g;
  var unescaperLookup = {
    '=0': '=',
    '=2': ':'
  };
  var keySubstring = key[0] === '.' && key[1] === '$' ? key.substring(2) : key.substring(1);

  return ('' + keySubstring).replace(unescapeRegex, function (match) {
    return unescaperLookup[match];
  });
}

This is probably indicative that somewhere I'm misusing React in my code, but since the stacktrace does not include references to any of my own code, I'm not sure what to look for. It seems to be an infinite loop of re-rendering, and I'm suspicious that it might be due to an improperly placed call to setState.

The Question

Is my suspicion likely? How can I further diagnose this issue, given that my own codebase is fairly extensive? What does it mean that this failed in KeyEscapeUtils?

like image 266
Scotty H Avatar asked Mar 24 '17 16:03

Scotty H


2 Answers

I searched for unescape through the source code of React (version 15.4) and have found only one place where it is used. The file is react/lib/flattenChildren.js:

function flattenSingleChildIntoContext(traverseContext, child, name, selfDebugID) {
  // We found a component instance.
  if (traverseContext && typeof traverseContext === 'object') {
    var result = traverseContext;
    var keyUnique = result[name] === undefined;
    if (process.env.NODE_ENV !== 'production') {
      if (!ReactComponentTreeHook) {
        ReactComponentTreeHook = require('./ReactComponentTreeHook');
      }
      if (!keyUnique) {
        process.env.NODE_ENV !== 'production' ? warning(false, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.%s', KeyEscapeUtils.unescape(name), ReactComponentTreeHook.getStackAddendumByID(selfDebugID)) : void 0;
      }
    }
    if (keyUnique && child != null) {
      result[name] = child;
    }
  }
}

It is in the flattenSingleChildIntoContext - exactly how it is shown in your stack trace. The line with the problem tries to show a warning:

process.env.NODE_ENV !== 'production' ? warning(false, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.%s', KeyEscapeUtils.unescape(name), ReactComponentTreeHook.getStackAddendumByID(selfDebugID)) : void 0;

It means that the maximum call stack error occurs only in development mode. And the problem is that you are rendering somewhere two or more elements with the same key. To know what key is that you can use breakpoint in this line in Chrome Dev Tools. Or you can add console.log there and throw exception so that it stops execution right there:

  if (!keyUnique) {
    console.log('key is not unique', name);
    throw new Error('Key is not unique');
  }

If you'll get the key this way you'll be able to find the place where you have elements with duplicate key.

like image 163
ischenkodv Avatar answered Oct 16 '22 11:10

ischenkodv


I don't think the place that it failed is significant, since you seem to be caught in an infinite loop, and this just happens to be the spot that the call stack limit is exceeded.

In order to diagnose, I would try turning on 'Pause on Exceptions' in chrome dev tools, if it's not already, and looking further up the stack trace for your own code when it pauses.

You are correct that improper setState calls could be causing this. For instance, if you were calling setState unchecked in componentDidUpdate or render. It's curious though that it only happens sometimes.

like image 29
TLadd Avatar answered Oct 16 '22 11:10

TLadd