CSSTransitionGroup (React) 抱怨找不到键

CSSTransitionGroup (React) complains about not finding key

我正在尝试使用 CSSTransitionGroup 为 SentenceList 中的 Sentences 设置动画。当按下 "next" 按钮时,我希望下一个句子动画化,列表中的第一个句子淡出。但是我收到此错误消息:

Each child in an array should have a unique "key" prop. Check the render method of Sentence.

我不明白为什么会这样,因为当我将 Sentence 推入我的列表时,我将它作为 "key" 道具传递给 {sentence.id}。 React 难道不应该知道每个句子键在渲染时都是这样定义的吗?

我尝试在 Sentence render 方法中再次定义键,但无济于事。我的 State 更改是否使 React 无法跟踪当前的 Sentence 键?

感谢您的帮助!

句子列表:

var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;


var SentenceList = React.createClass({
  getInitialState: function() {
    return {
      sentences: this.props.sentences
    }
  },
  //receives sentence and new blip from Sentence
  addBlip: function(sentence, value) {
    //see where in the loaded sentences we are
    var i = this.state.sentences.indexOf(sentence),
        sentences = this.state.sentences,
        // callback within a callback (post), the context changes inside the callback so we need to set this to self 
        self = this; 

    $.post(
      '/sentences/' + sentence.id + '/blips',
       {blip: {body: value}},
       //set sentence we blipped into as answered
       //reset state to reload sentences state after post
       function(response) {
         sentences[i].answered = true;
         // sentences[i].statistics = response.statistics;
         // put dummy content first then work it out in the backend to receive the format you want to receive (better to work from front to back)
         sentences[i].statistics = [
          {word: "butts", frequency: "95%"},
          {word: "dogs", frequency: "2%"},
          {word: "vegetables", frequency: "1%"},
          {word: "sun", frequency: "2%"}
         ];
         self.setState({sentences: sentences});
       });
  },

  //take in a sentence (sent from Sentence) and find current position in loaded sentences and set it to dismissed, then reload list
  dismissSentence: function(sentence) {
    var i = this.state.sentences.indexOf(sentence),
        sentences = this.state.sentences;

        sentences[i].dismissed = true;
        this.setState({sentences: sentences});
  },

  //list undismissed sentences and take out the first 3 for display
  topThreeRemainingSentences: function() {
    var unanswered = _.where(this.state.sentences, {dismissed: false}); 
    return unanswered.slice(0, 3);
  },

  render: function() {
    var remaining = this.topThreeRemainingSentences(),
        sentences = [],
        index = 0;

    //loop through sentences until we have 3 remaining sentences loaded    
    while (index <= (remaining.length - 1)) {
      var sentence = remaining[index];
      sentences.push(
        <Sentence key={sentence.id}
                  isActive={index == 0}
                  isNext={index == 1}
                  isNnext={index == 2}
                  onDismiss={this.dismissSentence}
                  onSubmitBlip={this.addBlip}
                  details={sentence} />
      )
      index = index + 1;
    }


    return (
      <ReactCSSTransitionGroup transitionName="animate">
      <div>{sentences}</div>
      </ReactCSSTransitionGroup>

    )
  }
});

句子:

var Sentence = React.createClass({
  getDefaultProps: function() {
    return {
      onSubmitBlip: function() { console.log(arguments) }
    }
  },
  //pass sentence and new blip to submit function
  addBlip: function(e) {
    e.preventDefault();
    var blipBody = this.refs.newBlip.getDOMNode().value
    this.props.onSubmitBlip(this.props.details, blipBody);
  },

  //send sentence to List to set it to dismissed
  dismissSentence: function(e) {
    e.preventDefault();
    this.props.onDismiss(this.props.details);
  },

  render: function() {  
    var phrase = this.props.details.body,
        phrase_display = phrase.split("*"),
        before = phrase_display[0],
        after = phrase_display[1],
        positionClass,
        stats;

    if (this.props.isActive) {
      positionClass = "active-sentence"
    } else if (this.props.isNext) {
      positionClass = "next-sentence"
    } else if (this.props.isNnext) {
      positionClass = "nnext-sentence"
    }

    //find stats for sentence if answered from json and push them into array ["word", x%]
    if (this.props.details.answered) {
      var words = [];
      this.props.details.statistics.forEach(function(statistic) {
        words.push(<li className="stats-list"><span className="stats-list-word">{statistic.word} </span>
          <span className="stats-list-percent">{statistic.frequency} </span> </li>)
      })

      stats = <div><span className="stats-list-header">others said:</span> {words}</div>
    }

    if (this.props.isActive) {
      nextButton = <div className="next-button" onClick={this.dismissSentence}>V</div>

    }
    if (this.props.isNext) {
      nextButton = <div></div>
    }
    if (this.props.isNnext) {
      nextButton = <div></div>
    }



    return (
      <div className={"blipForm " + positionClass}>
        {before}

        <form onSubmit={this.addBlip}>
          <input type="text"
                 ref="newBlip" />
        </form>

        {after}
        {nextButton}
        <br/>
        <ul>{stats}</ul>
      </div>
      )
  }
});      

在 Sentence 组件的 render 方法中创建的 <li> 元素需要 key 属性:

this.props.details.statistics.forEach(function(statistic) {
  words.push(<li className="stats-list" key={statistic.id}>...</li>);
});