Reputation: 103
I have a simple form. You click an "Add Item" button and a textbox appears. On blur, the text entered in the textbox gets added to a state variable array. Click the "Add Item" button again, another textbox appears and so on.
For each textbox, there is also a "Remove Item" button. When this button is clicked, the current item is removed from the array and the current textbox is removed from the page.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
}
}
addItem() {
this.setState({
items: [...this.state.items, []]
}
)
}
removeItem(index) {
//var items = this.state.items;
var items = [...this.state.items];
items.splice(index, 1);
this.setState({
items: items
})
}
changeItem(e, index) {
var items = this.state.items;
items[index] = e.target.value;
this.setState({
items: items
})
}
render() {
return (
<div>
{
this.state.items.map((item, index) => {
return (
<React.Fragment key={index}>
<hr />
<Row>
<Col column sm="8">
<Form.Control
type="text"
name="item"
onBlur={(e) => this.changeItem(e, index)}
/>
</Col>
</Row>
<Row>
<Col column sm="8">
<Button
onClick={() => this.removeItem(index)}
variant="link"
size="sm">
Remove Item
</Button>
</Col>
</Row>
</React.Fragment>
)
})
}
<br />
<Button
onClick={(e) => this.addItem(e)}
variant="outline-info">Add item
</Button>
</div>
)
}
}
The problem I have is, although the array is successfully modified in removeItem(index)
, the textbox that gets removed from the page is always the last one added, not the one that should be removed. For example:
items: ['aaa']
items: ['aaa', 'bbb']
items: ['aaa', 'bbb', 'ccc']
items: ['bbb', 'ccc']
The page should show a textbox with bbb and one with ccc. But it shows:
How can I remove the correct textbox from the page?
Upvotes: 0
Views: 1305
Reputation: 824
There are a few problems with your code:
this.state
without using this.setState()
in the changeItem
function, I have changed it to var items = [...this.state.items];
index
as key
for a list item, that you are rendering using the this.state.items.map((item, index) => {...})
in <React.Fragment key={index}>
. This key
should be a unique identifier for the list item, usually, it is the unique id
from the database. Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
. However, in your case, since you don't have unique ids for the items, I am creating those using uuid
module. learn more about keys: https://reactjs.org/docs/lists-and-keys.html and https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318column
attribute from the Col
component, because it was giving some warningthis.state.items
from array of strings
to array of objects, where each object has a id and data, where data is text
Form.Control
component without using value prop. Suppose you added one list item, wrote something in the input, clicked on the add item
button again. at this point, since you changed focus, the onBlur
event would trigger, and your this.state.items
would change accordingly, so far, so good. BUT now when it re-renders the whole thing again, it is going to re-render the Form.Control
component, but without the value
prop, this component will not know what data to show, hence it will render as empty field. Hence, I added value
prop to this componentvalue
prop in Form.Control
component, react now demands that I add onChange
event to the component, otherwise it will render as read-only input, hence I changed onBlur
to onChange
event. There is no need for onBlur
to change the state value, when onChange
is already there.Here is the finished code:
import React from "react";
import { v4 } from 'uuid';
import { Button, Row, Col, Form } from "react-bootstrap";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
}
addItem() {
this.setState({
items: [...this.state.items, {data: "", id: v4()}]
});
}
removeItem(index) {
console.log("dbg1", index);
//var items = this.state.items;
var items = [...this.state.items];
items.splice(index, 1);
this.setState({
items: items
});
}
changeItem(e, index) {
console.log("dbg2", this.state.items);
var items = [...this.state.items];
items[index].data = e.target.value;
this.setState({
items: items
});
}
render() {
console.log("dbg3", this.state.items);
return (
<div>
{this.state.items.map((item, index) => {
return (
<React.Fragment key={item.id}>
<hr />
<Row>
<Col sm="8">
<Form.Control
type="text"
name="item"
value={item.data}
onChange={(e) => this.changeItem(e, index)}
// onBlur={(e) => this.changeItem(e, index)}
/>
</Col>
</Row>
<Row>
<Col sm="8">
<Button
onClick={() => this.removeItem(index)}
variant="link"
size="sm"
>
Remove Item
</Button>
</Col>
</Row>
</React.Fragment>
);
})}
<br />
<Button onClick={(e) => this.addItem(e)} variant="outline-info">
Add item
</Button>
</div>
);
}
}
export default App;
Upvotes: 3