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!
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With