24

I am running through a react tutorial on tutsplus that is a bit old, and the code doesn't work as it was originally written. I actually am totally ok with this as it forces me to learn more independently, however I have spent a while on a bug that I just can't figure out. The bug consists of not being able to pass on an objects key, which prevents my program from updating the state of the correct object.

First off here is the repo if you want to run this code and see it in action: https://github.com/camerow/react-voteit

I have a child component that looks like this:

var FeedItem = React.createClass({

  vote: function(newCount) {
    console.log("Voting on: ", this.props, " which should have a key associated.");

    this.props.onVote({
      key: this.props.key,
      title: this.props.title,
      description: this.props.desc,
      voteCount: newCount
    });
  },

  voteUp: function() {
    var count = parseInt(this.props.voteCount, 10);
    var newCount = count + 1;
    this.vote(newCount);
  },

  voteDown: function() {
    var count = parseInt(this.props.voteCount, 10);
    var newCount = count - 1;
    this.vote(newCount);
  },

  render: function() {
    var positiveNegativeClassName = this.props.voteCount >= 0 ?
                                    'badge badge-success' :
                                    'badge badge-danger';
    return (
      <li key={this.props.key} className="list-group-item">
        <span className={positiveNegativeClassName}>{this.props.voteCount}</span>
        <h4>{this.props.title}</h4>
        <span>{this.props.desc}</span>
        <span className="pull-right">
          <button id="up" className="btn btn-sm btn-primary" onClick={this.voteUp}>&uarr;</button>
          <button id="down" className="btn btn-sm btn-primary" onClick={this.voteDown}>&darr;</button>
        </span>
      </li>
    );
  }

});

Now when someone hits the vote button the desired behavior is for the FeedItem.vote() method to send an object up to the main Feed component:

var FeedList = React.createClass({

  render: function() {
    var feedItems = this.props.items;

    return (
      <div className="container">
        <ul className="list-group">

          {feedItems.map(function(item) {
            return <FeedItem key={item.key}
                             title={item.title}
                             desc={item.description}
                             voteCount={item.voteCount}
                             onVote={this.props.onVote} />
          }.bind(this))}
        </ul>
      </div>
    );
  }

});

Which should pass that key on throught the parent component's onVote function:

var Feed = React.createClass({

  getInitialState: function () {
    var FEED_ITEMS = [
      {
        key: 1,
        title: 'JavaScript is fun',
        description: 'Lexical scoping FTW',
        voteCount: 34
      }, {
        key: 2,
        title: 'Realtime data!',
        description: 'Firebase is cool',
        voteCount: 49
      }, {
        key: 3,
        title: 'Coffee makes you awake',
        description: 'Drink responsibly',
        voteCount: 15
      }
    ];
    return {
      items: FEED_ITEMS,
      formDisplayed: false
    }
  },

  onToggleForm: function () {
    this.setState({
      formDisplayed: !this.state.formDisplayed
    });
  },

  onNewItem: function (newItem) {
    var newItems = this.state.items.concat([newItem]);
    // console.log("Creating these items: ", newItems);
    this.setState({
      items: newItems,
      formDisplayed: false,
      key: this.state.items.length
    });
  },

  onVote: function (newItem) {
    // console.log(item);

    var items = _.uniq(this.state.items);
    var index = _.findIndex(items, function (feedItems) {
      // Not getting the correct index.
      console.log("Does ", feedItems.key, " === ", newItem.key, "?");
      return feedItems.key === newItem.key;
    });
    var oldObj = items[index];
    var newItems = _.pull(items, oldObj);
    var newItems = this.state.items.concat([newItem]);
    // newItems.push(item);
    this.setState({
      items: newItems
    });
  },

  render: function () {
    return (
      <div>
        <div className="container">
          <ShowAddButton displayed={this.state.formDisplayed} onToggleForm={this.onToggleForm}/>
        </div>
        <FeedForm displayed={this.state.formDisplayed} onNewItem={this.onNewItem}/>
        <br />
        <br />
        <FeedList items={this.state.items} onVote={this.onVote}/>
      </div>
    );
  }

});

My logic relies on being able to reconcile the keys in the onVote function, however the key prop is not being properly passed on. So my question is, how do I pass on they key through this 'one way flow' to my parent component?

Note: Feel free to point out other problems or better design decision, or absolute stupidities. Or even that I'm asking the wrong question.

Looking forward to a nice exploration of this cool framework.

camerow
  • 365
  • 1
  • 3
  • 12
  • 3
    `key` has a special meaning in React. It's not made available to the component via `this.props.key` afaik. You should use an other prop name for that. Or, if you actually want to use the value as key, add an additional prop with the same, which you can then access inside the component. See https://facebook.github.io/react/docs/multiple-components.html#dynamic-children – Felix Kling May 26 '15 at 18:04
  • Ahh, ok, that helped tremendously. I had read that documentation already but was a bit perplexed by it. So to clarify it's ok for me to define a prop `key` but it should look like ``? Or should I avoid `key` altogether? – camerow May 26 '15 at 18:24
  • 1
    Still getting this warning `Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of FeedList. See https://fb.me/react-warning-keys for more information.`, although changing all instances of `key` to `id` allowed me to pass that prop along. – camerow May 26 '15 at 18:30
  • 1
    Oh, having the *object property* `key` is perfectly fine. The *prop* `key` is special to React. So `` helps React with reconciliation the components. However, the concrete instance of `FeedItem` doesn't really have access to the value of `key`, and it shouldn't have, since it is of no concern to it. If you want to pass the id to the component so that it can access it, choose a different prop name. – Felix Kling May 26 '15 at 18:31
  • 1
    You have to set `key` as well: ``. It may seem redundant, but they serve different purposes: `key` is for React and `id` is for your component. – Felix Kling May 26 '15 at 18:32

1 Answers1

78

The key prop has a special meaning in React. It is not passed to the component as a prop, but is used by React to aid the reconciliation of collections. If you know d3, it works similar to the key function for selection.data(). It allows React to associate the elements of the previous tree with the elements of the next tree.

It's good that you have a key (and you need one if you pass an array of elements), but if you want to pass that value along to the component, you should another prop:

<FeedItem key={item.key} id={item.key} ... />

(and access this.props.id inside the component).

Alexander Nied
  • 12,804
  • 4
  • 25
  • 45
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • 1
    Thank you. This solved my keying problem, now I just need to fix the logic in my `onVote` function. – camerow May 26 '15 at 18:40
  • Thanks a lot @Felix. BTW, can you help with this question also? I want to pass node as property in React way: http://stackoverflow.com/questions/34879973/pass-this-refs-as-property-in-jsx-in-react-js – haotang Jan 20 '16 at 13:11