Reputation: 861
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>
<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
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.
Upvotes: 0
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