5

As the title states, using React.cloneElement inside React.Children.map is causing element keys to change.

Here is a sandbox demonstrating this.

React.Children.map(children, (child) => {
    let clonedEl = React.cloneElement( child );
    console.log(clonedEl);
    return clonedEl;
});

The result of that block of code has elements with .$ added to the front of every key. This is really confusing for two reasons.

1: The documentation says that cloneElement will preserve keys and refs.

Clone and return a new React element using element as the starting point. The resulting element will have the original element’s props with the new props merged in shallowly. New children will replace existing children. key and ref from the original element will be preserved.

2: The results of the console.log is an element with preserved keys and ref...

This would lead me to believe that the addition is happening somewhere in the React.Children.map code.

UPDATE: After looking at the code for React.Children.map...

I figured out it is getting added by the following function chain: mapChilren -> mapIntoWithKeyPrefixInternal -> traverseAllChildren -> traverseAllChildrenImpl -> mapSingleChildIntoContext.

mapSingleChildIntoContext's third argument is childKey. It is called with nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar as it's third argument inside traverseAllChildrenImpl.

SEPARATOR = "." and getComponentKey returns the key with a $ prefixed to it within the escape function.

UPDATED PROBLEM:

Now I'm looking for a way around this... I'm not sure if there is one considering traverseAllChildrenImpl is called with an empty string as the nameSoFar within traverseAllChildren.

I think this may be intended the intended behavior of React.Children.map to build new DOM. This is causing a for me when trying to update the props on dynamic children.

SOLUTION: Don't use things how they're not intended to be used.

I was building a grouping of form controls that are really easy for the developer. The state tree is dynamically built by mapping the children and using . delineated string names from elements with names to create keys and values on the top level component.

The top level form component has onChange handlers for different types of controls and they are applied to the onChange properties of elements as needed. This mapping was done in the componentWillMount method and is what was causing me problems.

Moving the mapping to the render method allowed me to not have to update the children in the handles. Updating in the handles was causing elements to lose focus. All is good now!

Kyle Richardson
  • 5,567
  • 3
  • 17
  • 40
  • Not exactly an answer to your question but you can use `React.Children.forEach` as an alternative. – Prakash Sharma Oct 31 '17 at 06:24
  • Good point, I could "manually" rebuild the children with forEach. I think that might have to be the work around. I think this may be intended behavior of map. – Kyle Richardson Oct 31 '17 at 06:25

1 Answers1

6

The problem is not the cloneElement that changes your keys. As written in the documentation, cloneElement preserves the original keys. Its the React.Children.map that adds a prefix to it. If you don't want the keys to change make use of forEach instead of map

This is an excerpt from the React Code:

function escape(key) {
  var escapeRegex = /[=:]/g;
  var escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  var escapedString = ('' + key).replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });

  return '$' + escapedString;
}

function getComponentKey(component, index) {
  // Do some typechecking here since we call this blindly. We want to ensure
  // that we don't block potential future ES APIs.
  if (
    typeof component === 'object' &&
    component !== null &&
    component.key != null
  ) {
    // Explicit key
    return escape(component.key);
  }
  // Implicit key determined by the index in the set
  return index.toString(36);
}

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  var {result, keyPrefix, func, context} = bookKeeping;

  var mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(
      mappedChild,
      result,
      childKey,
      emptyFunction.thatReturnsArgument,
    );
  } else if (mappedChild != null) {
    if (ReactElement.isValidElement(mappedChild)) {
      mappedChild = ReactElement.cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  var result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
Kyle Richardson
  • 5,567
  • 3
  • 17
  • 40
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Appreciate the answer, but I had already solved it by this point :) Even mentioned those exact functions! Going to give you credit anyhow considering the time you took! – Kyle Richardson Oct 31 '17 at 08:32
  • After a few hours just discovered that the key also gets modified when using `React.Children.toArray`. – Alan P. Jul 17 '19 at 20:16
  • Thank you for this. It bothers me that they don't warn you in the official documents that this could happen. You're warned of this for React.Children.toArray, but not for React.Children.map. Spent forever trying to understand why stuff was unecessarily remounting only to discover this. – Jordy May 13 '20 at 14:53
  • @Jordy Well yes this is something that you find when you really did deep. However remounting instead of re-render is almost always associated with keys – Shubham Khatri May 13 '20 at 15:46
  • @Shubham Khatri You're probably right. I'm currently experimenting doing weird stuff involving having refs to past children for the sake of creating my own deferred unmounting system. So most likely it's my fault that unwanted remounts are occuring :) – Jordy May 13 '20 at 16:39