4
votes

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!

1
Not exactly an answer to your question but you can use React.Children.forEach as an alternative.Prakash Sharma
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

1 Answers

6
votes

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;
}