Ben Smith
Ben Smith

Reputation: 861

Deleting Stateful element from ReactJS array

I am having issues with removing stateful elements in an React array. In the following example, if you click 'toggle' on the second element ('B'), you will see:

Entry id: 0 name: A Is Active Toggle Remove

Entry id: 1 name: B Is Not Active Toggle Remove

Entry id: 2 name: C Is Active Toggle Remove

If you now click 'Remove' on the first element ('A'):

Entry id: 0 name: B Is Active Toggle Remove

Entry id: 1 name: C Is Not Active Toggle Remove

B has changed, but all I wanted to do was delete A and not impact any other elements. I've got a key as per the docs, and have tried using a key with a string prefix but it didn't help.

Is there any way to do this without pushing state up to TestContainer? (In my real situation I have a lot of disparate elements with different types of states).

<div id="test"></div>

<script type="text/jsx">

  var TestLine = React.createClass({
      getInitialState: function() {
          return {active: true};
      },
      handleLineRemove: function(e) {
        this.props.processLineRemove(this.props.id);
      },
      toggleActive: function(e) {
        this.setState({active: !this.state.active};
      },
      render: function() {
        return (
          <p>
            Entry id: {this.props.id} name: {this.props.name}
            {this.state.active ? " Is Active " : " Is Not Active " }
            <a onClick={this.toggleActive}>Toggle</a>
            &nbsp;<a onClick={this.handleLineRemove}>Remove</a>
          </p>
        );
      }
    });

     var TestContainer = React.createClass({
        getInitialState: function() {
          return {data: {lines: ['A','B','C']}};
        },
        processLineRemove: function(index) {
              var new_lines = this.state.data.lines.slice(0);
              new_lines.splice(index,1);
              var newState = React.addons.update(this.state.data, {
                lines: {$set: new_lines}
              });
          this.setState({data: newState});
        },
        render: function() {
            var body =[];
            for (var i = 0; i < this.state.data.lines.length; i++) {
                body.push(<TestLine 
                          key={i}
                          id={i}
                          name={this.state.data.lines[i]}
                          processLineRemove={this.processLineRemove} />)
            }
            return (
                <div>{body}</div>
            );
        }
    });

React.render(<TestContainer />, document.getElementById('test'));
</script>

Upvotes: 1

Views: 1530

Answers (2)

Dhiraj
Dhiraj

Reputation: 33628

If you have a list of items and you know that items will be edited randomly, I would not create the keys like that. I would rather create keys that unique and will not overlap with other keys upon list change. A simple random key generator like this should suffice.

Why ? Lets go over each step.

First renders looks like this

Entry id: 0 name: A Is Active Toggle Remove

Entry id: 1 name: B Is Not Active Toggle Remove

Entry id: 2 name: C Is Active Toggle Remove

And second looks like this

Entry id: 0 name: B Is Not Active Toggle Remove

Entry id: 1 name: C Is Active Toggle Remove

Now it is React's time to take these two, and figure out what changed. It will compare the components and the keys.

It looks at the first element and understands that the "active" flag is set to true based on the key. So it will apply the same to the key 0 that is finds in the second render. So on and so forth for the remaining elements.

Thus if your keys do not overlap upon change you can avoid this issue. This is the only way AFAIK. Hope this helps.

Here is a DEMO explaining the above.

Courtesy

Upvotes: 0

Brigand
Brigand

Reputation: 86270

When you have a list with arbitrary removals/insertions, you can't use index as a key. In this case, you could use the actual value because it isn't changed.

var body = this.state.data.map(function(x, i){
    return <TestLine 
        key={x}
        name={x}
        processLineRemove={this.processLineRemove.bind(null, i)} />
}.bind(this));

In more complicated case, you need an array of objects, and you assign a property with a unique value to each. Assuming state is [makeItem('A'), makeItem('B'), ...]:

function unique(){ return ++unique.i };
unique.i = 0;

function makeItem(text){ return {text: text, id: unique()}; };

var body = this.state.data.map(function(x, i){
    return <TestLine 
        key={x.id}
        name={x.text}
        processLineRemove={this.processLineRemove.bind(null, i)} />
}.bind(this));

Upvotes: 1

Related Questions