No key collision when returning identical arrays? - javascript

I recently posted an answer to a question when I made an observation about key's when returned from an array.
I already know that keys must be unique among sibling components. This is stated in the official React documentation, here:
Keys used within arrays should be unique among their siblings. However they don't need to be globally unique. We can use the same keys when we produce two different arrays.
However, in contrast to the bold text above, it seems like React is fine with same keys for identical arrays. How come the following snippet works and the keys don't collide?
class MyApp extends React.Component {
renderArray = () => {
return [
<p key={0}>Foo</p>,
<p key={1}>Bar</p>,
<p key={2}>Baz</p>
];
}
render() {
return (
<div>
{this.renderArray()}
{this.renderArray()}
{this.renderArray()}
</div>
);
}
}
ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Obviously, this doesn't work:
class MyApp extends React.Component {
render() {
return (
<div>
<p key={0}>Foo</p>
<p key={1}>Bar</p>
<p key={2}>Baz</p>
<p key={0}>Foo</p>
<p key={1}>Bar</p>
<p key={2}>Baz</p>
<p key={0}>Foo</p>
<p key={1}>Bar</p>
<p key={2}>Baz</p>
</div>
);
}
}
ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
So my question is, why does the first snippet work but not the second? It seems like they should both yield the exact same result?

Answer lays in how the createHierarchyRenderer method/function works. Source createHierarchyRenderer.js
It is reducingRight and appending Components by render to the conspect concating the result.
It creates renderHierarchy which is 2d array of components, so... Your renderArray invocations have different indexes in hierarchy which makes their hierarchies independant of eachother.
Second case is not working, because key's are pseudohierarchy to boost performance - you can do this without keying, but - it is very bad practice for bigger render queue. So, passing key - overwrites the dimension represented by key.
Bonus:
Why it works without passing keys? Which keys are the best? How to improve?!
Source:
/**
* Generate a key string that identifies a component within a set.
*
* #param {*} component A component that could contain a manual key.
* #param {number} index Index that is used if a manual key is not provided.
* #return {string}
*/
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 (component && typeof component === 'object' && component.key != null) {
// Explicit key
return KeyEscapeUtils.escape(component.key);
}
// Implicit key determined by the index in the set
return index.toString(36);
}
This method is called by traverseStackChildrenImpl method in the same file. If you will dig into these magic:
for (var i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseStackChildrenImpl(
child,
nextName,
callback,
traverseContext,
);
}
You will get why it is working, and you will know why the keying works without keys, and where you suffer performance looses. It is nice to pass key but... React can automagically pass the index for us, key is designed to take the id of component creator object... For example:
users.map((v, i) => (<UserItem data={v} key={i} />)
Makes no difference than not passing the key, but it is considered a bad practice:
users.map((v, i) => (<UserItem data={v} />)
key magic is designed to take id of user to not rerender rest of users in case of appending array inside fe. users = users.unshift(someuser).
The best available setup is:
users.map((v, i) => (<UserItem data={v} key={v.userId} />)

After considering the valuable input from the other answers posted here, I dug a bit deeper and re-created the two snippets in my own application, and then inspected the Component with the React Developer Tool.
I found the following:
First snippet:
In the first snippet where the renderArray() function is used, the children of the parent <div> does not have 9 <p> tags as children but rather 3 arrays, each containing 3 <p> tags.
This isn't so apparent at first, because the rendered output in the dev-tools (left-hand part), suggests 9 children, not 3.
Here I have expanded the first, fourth and seventh item, which are the items which I have given key as 0.
Second snippet:
In the second snippet where all <p> tags are placed directly as children of the <div> we get a similar output (if we ignore items 4-9 not showing, due to key collision), but the dev-tools show that the <div> has the expected 9 children. Hence the collision.
As with the previous snippet, I have expanded the first and fourth item where the key is 0. (forgot the seventh).
DL;DR
To summarize, when a React Component is given an array as children, the contents of that array do not get unpacked/destructed and given as children. Instead, the array remains as children and serves as a transparent container for its contents.
As such, there is no key collision because:
The parent <div> has 3 arrays as immediate children - not the 9 <p> components.
Each <p> component is "locked" within its parent array, because that's where it's defined. It obviously cannot suddenly move into one of the other arrays.
Each <p> component has a key inside its parent array (0-2); the only place collisions would be possible.

React keys must be unique for each element inside an instance of a parent. In your case, you have 3 instances of {this.renderArray()}, each with its own inner id. So children inside each instance have unique keys, but they are not siblings with children form other instances.
See this example from the official documentation: https://facebook.github.io/react/docs/lists-and-keys.html#keys-must-only-be-unique-among-siblings

Related

"You may have an infinite update loop in a component render function" when I add this line of code - console.log(perksTree.slots.unshift())

I have a function that find an object from a JSON that has an id === this.match.mainParticipant.stats.perkSubStyle. This object contains a property called slots that is an array and has 4 elements. Each slot has 3 elements which represent runes from a game. If you iterate over the slots and their elements you get this:
I get the object using this function:
secondaryPerks(){
let perksTree = this.$store.state.summonerRunes.find(value => value.id === this.match.mainParticipant.stats.perkSubStyle);
console.log(perksTree.slots.unshift())
return perksTree
}
and I iterate and display the icons using this:
<div v-for='runes in this.secondaryPerks().slots'>
<div v-for='rune in runes.runes'>
<img :src="'https://ddragon.leagueoflegends.com/cdn/img/' + rune.icon" alt="">
</div>
</div>
Now the problem is that because that perks tree is secondary one, the perks in slot[0] can never be picked because if they were picked, they'd have to be part of the primaryPerks tree. This means there's no point displaying that none of them were selected. For that reason I am trying to remove the first slot[0] element from the array, however, when I try to unshift() it, I get an error:
"You may have an infinite update loop in a component render function"
And I have no clue why. Any advices?
Firstly, I think you mean shift rather than unshift. unshift will try to add items to the array rather than removing them. It doesn't actually matter from the perspective of the infinite loop, either method will have the same effect.
You're creating a dependency on the array and then modifying it. Modifying it will trigger a re-render.
Each time the component re-renders it will shift another item onto/out of the array. Even if the call to shift/unshift doesn't actually change anything it will still count as modifying the array.
Try:
computed: {
secondaryPerkSlots () {
const perksTree = this.$store.state.summonerRunes.find(
value => value.id === this.match.mainParticipant.stats.perkSubStyle
);
return perksTree.slots.slice(1)
}
}
with:
<div v-for='runes in secondaryPerkSlots'>
That will create a new array containing the same elements as the original array, omitting the first element.
Alternatively you could put the slice(1) directly in the template:
<div v-for='runes in secondaryPerks().slots.slice(1)'>
Either way I suggest changing the method to a computed property instead. You should also drop the this in your template.
I had the same problem a few months ago.
I think the main issue is that you perform logic such as arr.unshift()(which will cause the template to re-render in this case) in your computed property.
So, imagine this:
const arr1 = [/* ... */];
// This is different
const computedArr = () => {
return arr.filter(() => { /* ... */ });
};
// Than this
const computedArr = () => {
const newArr = arr.filter(() => { /* ... */ });
// Vue cannot allow this without a re-render!
newArr.unshift();
return newArr;
};
The latter will cause the template to re-render;
EDIT
Check the first comment!

Different ways to add a key to JSX element in loop in React

I have been working on react for more than an year now. I have mostly played with iterating an array using .map, .forEach, .filter or using Object.keys and Object.values if it is an object.
But what are the different ways to add a unique key to jsx element. Below is what I have been used to till now
Using unique id from data as key to key prop:
const data= [{"id": "01", "name": "abc"}, {"id": "02", "name": "xyz"}];
render(){
const items = data.map(item => {
return <span key={item.id}>{item.name}</span>;
}
return(
<div>
{items}
</div>
)
}
Using index as key to key prop:
const data= [{"id": "01", "name": "abc"}, {"id": "02", "name": "xyz"}];
render(){
const items = data.map((item, i) => {
let keyValue = i+1;
return <span key={keyValue}>{item.name}</span>;
}
return(
<div>
{items}
</div>
)
}
Is there any other ways to add a unique key to the jsx element apart from what I have mentioned above and which is most efficient and recommended?
First of all, avoid using random keys.
There are a lot of ways to write keys, and some will perform better than others.
To understand how the keys we've chosen impacts on performance, it's necessary to understand React's Reconciliation Algorithm.
https://reactjs.org/docs/reconciliation.html
tl;dr Introduces a heuristic for comparing Virtual DOM trees to make this comparison O(n), with n the nodes of this VDOM tree. This heuristic can be split in these points:
Components of different type will create a new tree: This means that, while comparing the old tree with the new one, if the reconciler encounters that a node did change its type (e.g. <Button /> to <NotButton />), will cause to our Button to be unmounted with its children as well, and NotButton to be mounted with its children, as well.
We can hint React on how instances are preserved on VDOM, by avoiding recreating them. These hints are provided by us with keys.: After deciding if the instance in a node should be preserved (because its type remains the same), the reconciler will iterate on that node's children to compare them.
Supose now that we have this:
<div>
<Button title="One" />
<Button title="Two" />
</div>
And we'd like to add a Button to the DOM on the next render, say
<div>
<Button title="Zero" />
<Button title="One" />
<Button title="Two" />
</div>
The algorithm will go as follows:
Compares <divs> in both VDOMs. Since these have the same type, we don't need to recreate the new tree. Props are the same so there are no changes to apply to the DOM at this point.
Button One compares against Zero. Reconciler detects that here was a props change, then updates the DOM with this title.
Button Two compares against One. Reconcilier also detects a props change here and uses the DOM to write this change.
Detects that a new Button is added as last child, so creates a new Button instance at VDOM and write this change at DOM.
Notice that these has many operations on the DOM, because it compared components by their index.
Now, we can fix this behavior by letting know to our reconciler that these instances should be reused. Now, let's have this:
<div>
<Button title="One" key="One" />
<Button title="Two" key="Two" />
</div>
And we'd like to add a Button to the DOM on the next render, say
<div>
<Button title="Zero" key="Zero" />
<Button title="One" key="One" />
<Button title="Two" key="Two" />
</div>
The algorithm will go as follows:
Compares <divs> in both VDOMs. Since these have the same type, we don't need to recreate the new tree. Props are the same so there are no changes to apply to the DOM at this point.
Takes the first child of childrens. 'It's a Button', says the reconciler. 'And has a key' ('One'). Then, seeks for a children whose key is the same in the new children list. 'Oh, I encountered it!' but the reconciler realizes that there is no change on its props. Then, no DOM operations will be needed for this one.
The same scenario occurs with the second Button, it will compare by keys instead of by index. Realizes that it's the same instance and no props were changed, so React decides to not apply changes on the DOM.
For the Button with 'Zero' key, since there no exists a child with the same key, realizes that an instance should be created at VDOM, and this change should be written on DOM.
So, using keys by predictable contents helps the reconciler to perform less operations on the DOM. Healthy keys are those that can be inferred from the object that is being mapped, like a name, or an id or even an url if we are transforming urls to <imgs />.
What about key=index? Will have no effect, since by default, reconciler compares by position, i.e. its index.
These keys should be globally unique? Not necessarily. These should be unique among siblings, so reconciler can distinguish them while iterating by a node's children.
What about random keys? These should be avoided at all costs. If a key changes on every render, this will be keeping React destroying and creating instances on the VDOM (and hence, making extra writes on the DOM) since a component with a key wasn't found among the new children, but a new one with the same type.
If the render output is like
<div>
<Button key={randomGenerator()} />
</div>
Then, each time render is executed (e.g. due a props/state change, or even if it's parent is being re-rendered and our shouldComponentUpdate returns true), a new randomGenerator() key will be generated. This will go like:
'Hey! I've found a Button with a F67BMkd== key, but none was found in the next one. I'll delete it.'
'Oh! I've encountered a Button with a SHDSA++5 key! Let's create a new one'.
Whenever the reconciler tells that an instance should be deleted and unmounted, its internal state will be lost; even if we mount it again.
The instance at VDOM will not be preserved in this case.
The Button was the same, but the reconciler did a mess at DOM.
Hope it helps.
The best way to pick a key is to use a string that uniquely identifies a list item among its siblings. Most often you would use IDs from your data as keys:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
Also note:
Keys used within arrays should be unique among their siblings. However they don’t need to be globally unique.
However real answer to your question lives here: https://medium.com/#robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
There are many libraries that generate random unique ids such as shortid or uuid (which is the most popular one, just look at the number of downloads) or else just create your own function that generates random strings.
You can add them directly into object in array
const todos = [
{
id: uuid(),
text: 'foo',
}
]
and iterate like so:
const todoItems = todos.map(({id, text}) =>
<li key={id}>
{text}
</li>
);
md5 sha1 or even sha256 on the content.
You can use Date.now() with index, your code will be
as Ex.
const data= [{"id": "01", "name": "abc"}, {"id": "02", "name": "xyz"}];
render(){
const items = data.map((item, i) => {
let keyValue = Date.now()+i;
return <span key={keyValue}>{item.name}</span>;
}
return(
<div>
{items}
</div>
)
}

ReactJS - Passing an element of an array of Objects as props

I am building aReact Application.I have a state array which holds array of blogs.
I am rendering each blog using a BlogDisplay component.
Following is my code :
render() {
for( var i = 0;i < this.state.blogData.length;i++){
blogDataToRender.push(<BlogDisplay blogData = {this.state.blogData[i]} onUserTriggerForDetailedView ={this.props.onUserTriggerForDetailedView}/>);
}
return (
<div>
{blogDataToRender}
</div>
);
}
I am getting a warning like this :
index.js:5279 Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `BloggerHome`. See https://facebook.github.io/react/docs/lists-and-keys.html#keys for more information.
in BlogDisplay (created by BloggerHome)
in BloggerHome (created by Blogger)
in div (created by Blogger)
in div (created by Blogger)
in Blogger
Is my modelling correct ? How can I correct this?
Thanks a lot.
You need to add a key for each BlogDisplay component:
for( var i = 0;i < this.state.blogData.length;i++){
blogDataToRender.push(<BlogDisplay key={i} blogData = {this.state.blogData[i]} onUserTriggerForDetailedView ={this.props.onUserTriggerForDetailedView}/>);
}
The key is important for child components in an array. Because when one of those child components is under operation, React needs the key to determine which one is operated. Also, when the source array data is changed, React needs the key to check the diff of components.
For in-depth explanation about why keys are necessary, please refer to the official document.
What should be the value of key for each child component?
According to the above document:
The key only has to be unique among its siblings, not globally unique.
Also, blogData[i].id is the best choice if there is a unique id in the source data. Indexes as in the above example is not recommended, if the child components would reorder (performance issue).
Each element in React needs to have a unique key, which React uses to identify it. When you just render an element overtly, React automatically assigns one. However, when you assign an ARRAY of elements, each element in that array isn't assigned a key by default, so you need to. Something like this:
blogDataToRender.push(<BlogDisplay key={i} blogData={this.state.blogData[i]} onUserTriggerForDetailedView={this.props.onUserTriggerForDetailedView}/>);
Check out Chris' reply on Understanding unique keys for array children in React.js.
To fix this, you must set a unique id for each of your elements and then add a key prop to each element. You can read more about how React uses keys to identify elements in lists in the official documentation.
This is how your render() method should look:
render() {
return (
<div>
{this.state.blogData.map((item) => {
return (<BlogDisplay key={item.id} blogData={item} onUserTriggerForDetailedView={this.props.onUserTriggerForDetailedView} />)
})}
</div>
);
}
To answer your question quickly all you need to do is read the error and fix it! Add a unique key value to each component like so:
...
for( var i = 0;i < this.state.blogData.length;i++){
blogDataToRender.push(
<BlogDisplay
key={this.state.blogData[i].id}
blogData={this.state.blogData[i]}
onUserTriggerForDetailedView{this.props.onUserTriggerForDetailedView}
/>
);
}
...
In this case I assume that each one of your blog entries has a unique id, the thing you need to be aware of here is that the key value needs to be unique and not depending on the position of that element inside the array. Every time you render these elements that key needs to be the same, this value is used by React to optimize re rendering and avoid rendering items that did not change. If you want to understand why this is necessary here is a good link. Next time I recommend reading the docs because they are very well done and you can learn a lot from it.
Now the way I would do it is using map:
...
const blogDataToRender = this.state.blogData.map(blogData =>
<BlogDisplay
key={blogData.id}
blogData={blogData}
onUserTriggerForDetailedView{this.props.onUserTriggerForDetailedView}
/>
);
...
This helps readability and leverages an iterator so you don't need to keep track of the index. It is a widely used pattern when developing in React.

React cloneElement not setting key

I'm building a table control which dynamically generates keys (which I understand may not be a good idea - I imagine the key should be uniquely associated with the data it represents otherwise React could just generate unique ids for us?), but either way it seems the keys are not being set and I have no idea why. The rows in the table are generated with a function that can be found here. Basically I have a helper component which takes an optional component to transform all child elements - here is one of those transform functions (from the TableBody component):
const transformRows = (keyPrefix) => (children, passthroughProps) => (React.Children.map(children, (c, i) => {
return React.cloneElement(c, {
key: `${keyPrefix}-${i}`,
style: rowStyle,
className: rowClassName,
columnDefinitions: columnDefinitions,
rowData: tableData[i],
includeVerticalScrollbar,
...passthroughProps
});
}));
the weird thing is that when I step through the code it seems the key is being assigned, but then I get a warning in the browser Each child in an array or iterator should have a unique "key" prop which traces back to here in the stack trace.
Any ideas?
One way to overcome this is to wrap each cloned element with React.Fragment and set the key on the latter.
Like so:
<>
{connections.map(connection => (
<React.Fragment key={connection.id}>
{React.cloneElement(element, connection)}
</React.Fragment>
))}
</>
from the docs React.cloneElement
Unlike React.addons.cloneWithProps, key and ref from the original
element will be preserved.
So if you set key for Cell, than each clone get key automaticaly.

Child array unique key in ReactJS using a non-child component

So I understand the unique key: if I have something like:
array.push(<ChildComponent key=some_key />
But if I have something like this:
cols.push(<td key={i}>Some value</td>)>
(While I am creating a table component that has pagination...). I am not sure about that key... That table supports sorting (for columns) so... I believe when we sort, if we use that "i" key, something will be broken. Is in this case, a key necessary? Or it is only necessary in an array that contains child components. In any case, if we don't use a key we get a warning from React that we want to avoid/fix.
Index based keys are sufficient unless:
items are inserted at or removed from anywhere except the end
the order of items in the items can change
items can be replaced with other items
If any of those are true, you should use a key which identifies the items themselves, rather than their position in the array. Otherwise the index sufficiently identifies it.
Using keys correctly has performance improvements when the subtrees are large or involve img/video/other expensive components. It's also a requirement if you want to animate items being added/removed/reordered correctly.
So if I understand you correctly, "i" is some index of the sorted array? If so, that will break down because components should be keyed to something unique to their data (and thus their rendering), not something that is varient like an index.
Because the key isn't consistently tied to the same piece of data, this will simply cause the div's to rerender their contents, as opposed to simply rearranging their order. Try setting key to a hash based on their data!
If you're looking for a nice solution to generating unique keys, I plucked this one from some of the React/Flux examples and it's worked great for my team and I.
uuid: function () {
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
}
return uuid;
}
Keys are about state. By state I mean not justthis.statebut also anything you store directly inthisand need or want to preserve between renders. If your children do not have state, keys do not matter. You could generate a random key every time for each child and that would work. But if some children have state, that approach will create a new child, initialized to its initial state. So if you need to preserve child state between renders the child key cannot change. This will ensure that the child component is preserved rather than recreated.
See Child Reconciliation here: http://facebook.github.io/react/docs/multiple-components.html

Categories