33

I have created a React Component that renders a set of sub-elements given an array of ids. The array of ids is kept in the state of the parent component, and then I run some ajax calls based on the ids to fetch data to render. The fetched data is stored in a separate data array in the state. The rendered components use the id as key.

The ids can change based on actions outside of the component, so I use setState on the component to replace the array. The updated id-state will probably contain some of the same ids as the in the original array. At the same time I empty the 'data array' so that everything will be rendered again.

When I do this I sometimes get the key-warning:

Warning: flattenChildren(...): Encountered two children with the same key. Child keys must be unique; when two children share a key, only the first child will be used.

The new array does not contain any duplicates. So why does it happen, and what can I do to avoid this?

Edit: Added some code by request. Note: I am using the Infinite Scroll module. Could this be causing it?

Initial state:

getInitialState: function() {
  return {
    hasMore: true,
    num: 0,
    movieIds: this.props.movieIds,
    movies: []
  };
},

Render function:

render: function() {
  var InfiniteScroll = React.addons.InfiniteScroll;

  return (
    <InfiniteScroll
        pageStart={0}
        loadMore={this.loadMore}
        threshold='20'
        hasMore={this.state.hasMore}>
        <ul className="movieList">
          {this.state.movies}
        </ul>
    </InfiniteScroll>       
);
}

Simplified load more:

comp = this;
$.ajax( {
  url: url,
  contentType: "json",
  success: function (data) {
    var m = createMovieLi(data);
    var updatedMovies = comp.state.movies;
    updatedMovies[num] = m;
    comp.setState({movies: updatedMovies});
  }
});

And finally when updating outside the component:

movieBox.setState({
  hasMore: true,
  num: 0,
  movieIds: filteredIds,
  movies: []
});
Øyvind Holmstad
  • 1,379
  • 2
  • 13
  • 22
  • Give a minimal example of code that shows the problem. It's probably a simple fix, but entirely dependant on your code. – Brigand Sep 28 '14 at 19:55

2 Answers2

20

I figured out my mistake, and it had nothing to do with React per se. It was a classic case of missing javascript closure inside a loop.

Because of the possibility of duplicates I stored each ajax response in window.localStorage, on the movieId. Or so I thought.

In React Inifinite Scroll each item in your list is drawn sequentially with a call to the loadMore-function. Inside this function I did my ajax call, and stored the result in the browser cache. The code looked something like this:

  var cachedValue = window.localStorage.getItem(String(movieId));
  var cachedData = cachedValue ? JSON.parse(cachedValue) : cachedValue;

  if (cachedData != null) {
    comp.drawNextMovie(cachedData);
  } else { 
    $.ajax( {
      type: "GET",
      url: this.state.movieUrl + movieId,
      contentType: "json",
      success: function (movieData) {
        window.localStorage.setItem(String(movieId), JSON.stringify(movieData));
        comp.drawNextMovie(movieData);
      }
    });  
  }    

Can you spot the mistake? When the ajax-call returns, movieId is no longer what is was. So I end up storing the data by the wrong id, and get some strange React warnings in return. Because this was done inside the loadMore function called by the InfiniteScroll-module, I was not aware that this function was not properly scoped.

I fixed it by adding a Immediately-invoked function expression.

Community
  • 1
  • 1
Øyvind Holmstad
  • 1,379
  • 2
  • 13
  • 22
12

I wouldn't use the ID from a back-end as key property in React. If you do, you're relying on some logic that's a bit far away from your component to make sure that your keys are unique. If they keys are not unique, you can break react's rendering so this is quite important.

This is why you should, in my opinion, just stick to using the index within a for loop or similar to set key properties. That way you know they can never be non-unique, and it's the simplest way of doing exactly that.

Without knowing exactly how your IDs work it's impossible to say what's causing the non-unique clash here. However since key is just to allow React to correctly identify elements, and nothing else, it doesn't really make sense for it to be anything other than a simple count or index.

var children = [];
for (var i=0; i<yourArray.length; i++) {
    children.push(
        <div key={i}>{yourArray[i].someProp}</div>
    );
}
Mike Driver
  • 8,481
  • 3
  • 36
  • 37
  • 8
    The exception is if items can be reordered, or inserted/removed anywhere except for the end. – Brigand Sep 28 '14 at 22:22
  • Thanks for the input. It didn't solve my problem though. – Øyvind Holmstad Sep 29 '14 at 06:01
  • If you're using `React.Children.map ` to traverse a list of objects, how would you get an index number to use? Can you use anything from the `map` function? Or would you just create one yourself? – Ben Mar 12 '15 at 16:03
  • @BenGrunfeld the index will be passed as the second argument to the function you give to `map`. Example: http://jsfiddle.net/69z2wepo/4120/ – Max Heiber Mar 14 '15 at 03:11
  • @mheiber you do not even use key attribute at your example? – Teoman shipahi Jan 22 '16 at 05:12
  • @Mike Driver - There are some situations where I'd disagree. If you have a key on the backend that is guaranteed unique (a DB id) and you only want it rendered once, this error can be helpful to id otherwise unnoticed bugs when instances are rendered multiple times. – Eric H. Feb 13 '16 at 17:14
  • this answer solve my problem, I've been using unique id from database as key, and I got this same key error. Switch to using element index as key do the job. – angry kiwi Apr 24 '16 at 06:57