3

I'm trying to learn React-Native. I'm looking at an example, Responsive image grid by Joshua Sierles. (Thank you Joshua!) In this sample, Joshua uses React to carefully place image elements in a controlled manner across the mobile display. Note: he uses ONLY three images and repeats them several times within the document. Unfortunately, as written, the example generates a warning:

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of YourProjectNameHere. See fb.me/react-warning-keys for more information. (Link extrapolated from the shortened form...)

I fully understand that each element within a row and each row generated by React must have a unique key property. What I'm not clear on is exactly how to do that. Here's my hack / work around. key={Math.random()}

This hack works fine, but it just seems so... wrong. The question here is, what is the right way to identify individual image ID's as well as identify individual row ID's?

'use strict';

 var React = require('react-native');
 var {
   AppRegistry,
   StyleSheet,
   Text,
   View,
   Image,
   Dimensions,
   ScrollView
 } = React;

 var _ = require('lodash');
 var {width, height} = Dimensions.get('window');

 var IMAGE_URLS = _.flatten(_.times(9, () => {return ['http://rnplay.org/IMG_0599.jpg', 'http://rnplay.org/IMG_0602.jpg', 'http://rnplay.org/IMG_0620.jpg']}));  // 9 x 3 = 27 images
 var IMAGES_PER_ROW = 4;

 var AwesomeProject1 = React.createClass({

   getInitialState() {
     return {
       currentScreenWidth: width,
       currentScreenHeight: height
     }
   },

   handleRotation(event) {
     var layout = event.nativeEvent.layout
     this.setState({currentScreenWidth: layout.width, currentScreenHeight: layout.height })
   },

   calculatedSize() {
     var size = this.state.currentScreenWidth / IMAGES_PER_ROW
     return {width: size, height: size}
   },


   // note:  I added key={Math.random()} in two places below.
   // Its a BS fix, but it seems to work to avoid the warning message.
   renderRow(images) {
     return images.map((uri) => {
       return (
         <Image style={[styles.image, this.calculatedSize()]} key={Math.random()} source={{uri: uri}} />  //key={Math.random()}
       )
     })
   },

   renderImagesInGroupsOf(count) {
     return _.chunk(IMAGE_URLS, IMAGES_PER_ROW).map((imagesForRow) => {
       return (
         <View style={styles.row} key={Math.random()}>
           {this.renderRow(imagesForRow)}
         </View>
       )
     })
   },

   render: function() {
     return (
       <ScrollView onLayout={this.handleRotation} contentContainerStyle={styles.scrollView}>
         {this.renderImagesInGroupsOf(IMAGES_PER_ROW)}
       </ScrollView>
     );
   }
 });

 var styles = StyleSheet.create({

   row: {
     flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'flex-start'
   },

   image: {
   }
 });

 AppRegistry.registerComponent('AwesomeProject1', () => AwesomeProject1);

I've tried every combination of key=uri.id , imagesForRow.id, images.id, etc that I can think of. None works as good as the Random Number function. Other ideas? What's the right way to do this?


Update per Chris Geirman's answer below: I wanted to show my final code.

renderRow(images) {
     return images.map((uri, idx) => {
       return (
         <Image style={[styles.image, this.calculatedSize()]} key={uri.concat(idx)} source={{uri: uri}} />  //key={Math.random()}
       )
     })
   },

   renderImagesInGroupsOf(count) {
     return _.chunk(IMAGE_URLS, IMAGES_PER_ROW).map((imagesForRow, idx2) => {
       return (
         <View style={styles.row} key={imagesForRow.concat(idx2)}>
           {this.renderRow(imagesForRow)}
         </View>
       )
     })
   },
Community
  • 1
  • 1
zipzit
  • 3,778
  • 4
  • 35
  • 63

2 Answers2

5

I think the key is only really relevant if you're going to move components around (e.g. change the order) or prepend elements. If that's not an issue for you and you just want to silence the warning you can use the array index (second arg to map()).

Short official explanation of React reconciliation algorithm is in React Docs - Listing Mutations.

Expanded answer

I'm going to expand on this, because I believe the other answer, that you accepted, is misleading: not a legit solution as it presents itself.

My understanding is that React interprets an element child that's an array as a special case where the length or relative position of elements may change while component instances need to be preserved. In the React docs they describe it like this:

when the children are shuffled around (as in search results) or if new components are added onto the front of the list (as in streams).

But often an array is used just for convenience for scripting generation of children that could otherwise be expressed literally / statically. And that's the case I think you have here. If that wasn't the case you would need and would likely already have legit unique IDs. For example, if I understand your use case correctly, you could just do something like this, in which case there'd be no key warning from React:

var image_urls = [
  'http://rnplay.org/IMG_0599.jpg',
  'http://rnplay.org/IMG_0602.jpg',
  'http://rnplay.org/IMG_0620.jpg'
];

{/* #1 */}
<View>
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[0]}} />
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[1]}} />
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[2]}} />
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[0]}} />
</View>

{/* ... */

{/* #n */}
<View>
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[0]}} />
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[1]}} />
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[2]}} />
  <Image style={[styles.image, this.calculatedSize()]} source={{uri: image_urls[0]}} />
</View>

Obviously that would be a horrendous way to do it, so hence the use of arrays. But React doesn't distinguish this use of array (to script the equivalent of that) from the other case (where the length and order of elements is dynamic) and complains about the key. You could just ignore the warning, but it's easily silenced by just assigning the index value of the element.

Simplified example. These variations produce equivalent output, but differ by being literally declared or scripted and emitting a key warning or not.

var container = document.getElementById('container');
var items = ["A", "B", "C"];
var groups = Array(2).fill(items);

function render (element) {
  ReactDOM.render(
    element,
    container
  );
}

console.log("literal, no key warning");

var element = <div>
  <div class="items">
    <div>{items[0]}</div>
    <div>{items[1]}</div>
    <div>{items[2]}</div>
  </div>

  <div class="items">
    <div>{items[0]}</div>
    <div>{items[1]}</div>
    <div>{items[2]}</div>
  </div>
</div>;

render(element);


console.log("scripted, no key warning");

var element = <div>
  {groups.map((items, i) =>
    <div class="items" key={i}>
      {items.map((item, j) =>
        <div key={j}>{item}</div>
      )}
    </div>
  )}
</div>;

render(element);


console.log("scripted, with key warning");

var element = <div>
  {groups.map((items, i) =>
    <div class="items">
      {items.map(item =>
        <div>{item}</div>
      )}
    </div>
  )}
</div>;

render(element);
Jurosh
  • 6,984
  • 7
  • 40
  • 51
JMM
  • 26,019
  • 3
  • 50
  • 55
  • Valid response, but I really want to learn how to do this right in the future (and understand why that is the right thing to do...) – zipzit Apr 24 '16 at 02:49
  • Then keep in mind that A) the other answer isn't actually using a unique key per component, it's still using the index. If these components were stateful or something and you changed the relative positions of some with the same URI, it wouldn't work. If the numbers of elements and their position within the array aren't going to change during the life of the page in this case, then index would be sufficient. If those things are going to change and you actually need to uniquely identify components then you'd need truly unique IDs anyway ¯\\_(ツ)_/¯ B) Doesn't address the `` components. – JMM Apr 24 '16 at 13:00
  • Right you are. I do recognize that this is a contrived example. In reality, if you ever used this design in an app, you'd have unique images everywhere and a need to 'click' on 'em and do something. You guys have forced me to understand better why React needs the unique identifier. Many thx. – zipzit Apr 24 '16 at 15:12
  • 2
    Ok, well I don't think the other answer is "the right way", but you have it marked as accepted. – JMM Apr 24 '16 at 15:28
  • I gotta say.. I'm almost tempted to use a random function to define an id. :^) @JMM I'm very appreciative to both you and Chris for your responses here. I wish there was a way to accept both answers as acceptable. The check mark was at a point in time before your append. – zipzit Apr 24 '16 at 16:40
  • The scripted way will generate warnings now. I think React 14+ fixed that. – Mrchief Jan 13 '18 at 02:50
0

The "key" is how react's render determines whether the object assigned to that node has changed or not. It's an optimization tactic. If the same key is assigned to a particular node this render as was assigned last render, then it doesn't bother re-rendering. So you want each key to be unique to that node, but also to be predictable/consistent and therefore guessable so that every time you render that particular node, it will be assigned the exact same unique key.

Math.random() is a poor choice for a key because it fails the predictable/consistent criteria. Using the array index by itself is also a poor choice for similar reasons.

Instead, I would recommend using the array value + index

renderRow(images) {
     return images.map((uri, idx, arr) => {
       return (
         <Image style={[styles.image, this.calculatedSize()]} key={uri.concat(idx)} source={{uri: uri}} />  
       )
     })
   },
Chris Geirman
  • 9,474
  • 5
  • 37
  • 70
  • 1
    I didn't understand why you added the extra variable name `arr` in the line of code `return images.map((uri,idx, arr) => {...` so I just left it out. Was that an attempt to try something else, or a specific teaching point? Seems to be working fine, and yeah, I knew the random thing was bad, just bad. I will update my question to show the corrected code per this answer. Thx! – zipzit Apr 24 '16 at 02:42
  • He was just using the full method signature for `map`. – Interrobang Apr 24 '16 at 02:44
  • that's right, as @interrobang pointed out, I was just using the full signature. You can safely omit it if you choose. – Chris Geirman Apr 24 '16 at 02:49
  • This doesn't uniquely identify component instances, it still uses the index. This would only reliably work in cases where just the index would anyway. If you really needed to uniquely identify each component (e.g because it's stateful and they can be reordered) you'd need a unique ID anyway. And it says nothing about how to handle the `` components. – JMM Apr 24 '16 at 13:03
  • I disagree. Take thes three images (x2)... imgA.jpg+1, imgB.jpg+2, imgC.jpg+3, imgA.jpg+4, imgB.jpg+5, imgC.jpg+6. If I swap imgA and imgB in the array's index 1 & 2 spot, that would invalidate both because of the value+index key. If I based off the key only, neither would update. If I swap indexes 1 & 4, there would be no change, but since they're the same image, the net result would be exactly the same. – Chris Geirman Apr 24 '16 at 13:13
  • 2
    That's my point, this isn't a robust solution & in practice probably no better than using just index. It doesn't enable swapping two components representing `A` if you need to (e.g. if they're stateful). And swapping `A` and `B` shouldn't destroy the instances -- that defeats much of the purpose of `key`. If you really need to uniquely identify instances -- e.g. the number / position of elements can change -- you need a truly unique ID, which this isn't. If number and position are static for the life of the page then just index is sufficient. & this doesn't address `` components at all – JMM Apr 24 '16 at 13:51
  • @ChrisGeirman Interrobang "Full Method Signature for Map" --> [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) Wow. My head asplode. I had no idea that stuff was defined anywhere. Thank you for including the spark and assist. – zipzit Apr 24 '16 at 14:56
  • @jmm this is a contrived example. In reality, you'd probably be fine with using the uri alone. The reasoning still holds true. Don't get too hung up on the contrived example – Chris Geirman Apr 24 '16 at 15:40
  • "The reasoning still holds true." -- nope. You either have a unique ID or you don't. If you don't, and you don't need one, you can use index to silence the warning. If you do need one, this isn't it. I can't think of a situation where this would be the right answer. – JMM Apr 24 '16 at 16:38
  • By "the reasoning" I meant my explanation for why you want the key to be unique, consistent and predictable. Since in this case there is no unique key, uri+index is IMO the best one could do in the circumstances – Chris Geirman Apr 26 '16 at 01:47
  • @ChrisGeirman What do you get from combining index with some string? it's still random string = poor performance if there are reorders. If there is no unique identifier then use INDEX itself. – Jurosh Jul 13 '17 at 15:22
  • @Jurosh ideally you want your key to uniquely identify the image itself. Using an array's index alone is really meaningless, since it says nothing about the value stored there. However, combining the image uri with the index does. So, if "img1.jpg" is stored at array[1] and is given the key "img1.jpg-1" and it stays in array[1], then react will not rerender. If it's moved to array[2] and given the key "img1.jpg-2", then react will see it's changed and rerender. None of this is ideal... it's just maybe an arguably insignificant improvement over using index alone. – Chris Geirman Jul 13 '17 at 19:37
  • @ChrisGeirman React will re-render anyway - keys only helps with more efficient DOM updates. But, there might be issue with uncontrolled elements like , because those will be not be updated when not using proper unique keys. – Jurosh Aug 16 '17 at 18:22
  • @ChrisGeirman React doesn't use keys for change detection. Keys allow React to reuse the same component instances across multiple consecutive renders. See the official documentation : https://reactjs.org/docs/lists-and-keys.html#keys Could you maybe update your answer ? – Simon V. Jan 22 '18 at 09:47
  • @SimonV. literally the very first line of the new documentation you referenced is, "Keys help React identify `which items have changed`, are added, or are removed." – Chris Geirman Jan 22 '18 at 13:09
  • @ChrisGeirman Yes, I didn't express myself clearly enough, sorry. What I meant is that React doesn't use keys to determine whether a "keyed" component is "dirty" and should be rendered or not (like your answer suggests). It uses keys to determine whether component instances should be created/deleted/reused from the previous render. Your sentence `If the same key is assigned to a particular node this render as was assigned last render, then it doesn't bother re-rendering` looks incorrect to me. – Simon V. Jan 23 '18 at 08:29