I have a component that contains an array of items in its state, but whenever I try to delete an item, the wrong one is deleted.
Here's a simplified version of my parent Component:
export default class BankList extends Component {
state = {
banks: [new Bank("Name1"), new Bank("Name2")]
}
addBank() {
this.setState({banks: [...this.state.banks, new Bank("")]})
}
removeBank(index) {
let good = [];
this.state.banks.forEach((item, ind) => {
if(ind !== index){
good.push(item);
}
});
this.setState({banks: good});
}
render() {
return (
<Container>
<Content>
<List>
{this.state.banks.map((bank, index) => <BankContainer bank={bank} key={index} id={index} onRemove={() => this.removeBank(index)}/>)}
</List>
<Content>
<Right>
<Button onPress={() => this.addBank()}>
<Text>Add Bank</Text>
</Button>
</Right>
</Content>
</Content>
</Container>
)
}
}
The BankContainer class simply shows the Bank, and when the "remove" button inside it is pressed, it will call onChange with the provided id. I have verified that the right index is being passed into removeBank. But, after removeBank executes, the last item (index 1) is removed from the banks array, when I selected the first one (index 0). I have tried the following method bodies in removeBank, but to no avail:
Tried a shallow copy:
let old = [...this.state.banks];
old.filter((item, ind) => ind !== index);
this.setState({banks: old});
Tried a straight filter:
this.setState({banks: this.state.banks.filter((item, ind) => ind !== index);
Tried a deep copy:
let old = [...this.state.banks];
old = old.map(i => Object.assign({}, i));
old.filter((item, ind) => ind !== index);
this.setState({banks: old});
Tried a splice:
this.setState({banks: this.state.banks.splice(index, 1)});
Tried a shallow copy and splice:
let old = [...this.state.banks]
old = old.splice(index, 1);
this.setState({banks: old});
Tried a deep copy and splice:
let old = [...this.state.banks];
old = old.map(i => Object.assign({}, i));
this.setState({banks: old.splice(index, 1)})
None of these have worked, they have all exhibited the same behavior. I'm at my wits end on this, any suggestions would be hugely appreciated!
According to the docs:
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.
Using a stable ID, and using that to remove elements from the array, should solve your issues.
Related
SOLUTION: Update the key value for the input element to refresh the default value => content of the input element. Deleting an element from the array DID work. Thanks for your help!
src: https://thewebdev.info/2022/05/12/how-to-fix-react-input-defaultvalue-doesnt-update-with-state-with-javascript/#:~:text=state%20with%20JavaScript%3F-,To%20fix%20React%20input%20defaultValue%20doesn't%20update%20with%20state,default%20value%20of%20the%20input.
I got an useState array in my code which represents a lisst of students:
const [students, setStudents] = useState([""]);
This array gets mapped to student elements:
{students.map((student, index) => <Student setStudents={setStudents} students={students} id={index} key={index} content={student} />)} I also got an AddStudent element which adds students to the array.
function AddStudent(props) {
const {setStudents} = props;
return (
<button className="change-student add-student" onClick={() => {
setStudents((students) => [...students, ""])
}}>
+
</button>
);
}
The RemoveStudent component is supposed to remove a student by its index in the array. I've tried many different ways but none worked correctly. How can I get it to work? Here is my code:
function RemoveStudent(props) {
const {students, setStudents, id} = props;
return (
<button className="change-student remove-student" onClick={() => {
let data = students;
if(id > -1) {
data.splice(id, 1);
}
console.log(data)
// setStudents(data)
// alternative:
// setStudents(students.filter(index => index !== id)); // removes the last element in the list
// doesn't work properly
}}>
-
</button>
)
}
Thanks for your help!
2 things should be noted here:
While updating react state arrays, use methods that return a new array (map, filter, slice, concat),
rather than ones that modify the existing array (splice, push, pop, sort).
While updating React state using its previous value, the callback argument should be used for the state setter. Otherwise you may get stale values. (See React docs).
if(id > -1) {
setStudents(students=> students.filter((s,i)=>(i != id)))
}
Consult this article, for a complete reference about how to update React state arrays.
You need to copy the students array first and then try removing the student by index. I assume by id you mean index at which to remove the student. Then you can try something like:
function RemoveStudent(props) {
const {students, setStudents, id} = props;
return (
<button
className="change-student remove-student"
onClick={() => {
if(id > -1) {
const data = [...students]; // making a copy
data.splice(id, 1); // removing at index id
console.log(data)
setStudents(data)
}
}}
>
-
</button>
)
}
With array.filter() you have a mistake in how you pass callback to filter() method. Please try the following:
setStudents(students.filter((,index) => index !== id));
Notice the index is second param of the callback so I used a , before index.
After #Irfanullah Jan 's answer you should make sure how you show the student.
Here is the simple example:
const [students, setStudents] = useState([1, 2, 3]);
return (
<div>
{students.map((student, index) => {
return <div>{student}</div>; // show the value not the index
})}
<button
onClick={() => {
let id = 1;
const copy = [...students];
copy.splice(id, 1)
console.log(copy)
setStudents(copy);
}}
>
-
</button>
</div>
);
The code above will delete the student of "index==1"
I am looking to filter through an array and return all elements of the array except the element which has been clicked on, so I have a map of list elements each with a key={index} of their map, onClick it should call my remove function, and pass in the index of the element to be removed, I then need to filter over that array, remove the element, update state, and send this information to my backend.
here is the delete function
const deleteItem = (id) => {
// use filter, to loop through all pieces of index
const element = list.todoItems.indexOf(id - 1);
setList({ todoItems: list.todoItems.filter(element !== id) });
console.log(list.todoItems);
dispatch(updateTodo(list));
};
here is the mapped array
{list.todoItems.map((Item, index) => (
<div
// setup anonymous function, that will call
// ONLY when the div
// is clicked on.
key={index}
onClick={() => deleteItem(index)}
>
{/* list item, gets text from props */}
<li>{Item}</li>
</div>
))}
I must be missing something, because this should work, Though i may have to shift gears and have each item as an actual object in my database, though id rather not do this as i feel an array of strings is more than appropriate for this app.
Remove your indexOf logic and this will work.
You don't have to find the index of the array because you're already receiving it as a parameter.
const deleteItem = (id) => {
setList({ todoItems: list.todoItems.filter((_, filterID) => filterID !== id) });
console.log(list.todoItems);
dispatch(updateTodo(list));
};
You don't need to subtract one from the index on the indexOf function
And for this case, splice works better than filter
const deleteItem = (id) => {
const element = list.todoItems.indexOf(id);
setList({ todoItems: {...list}.todoItems.splice(element, 1)});
dispatch(updateTodo(list));
};
This is from a react native tutorial -> https://www.youtube.com/watch?v=qSRrxpdMpVc
The app is for putting down what your 'course goals' are. It's basically just a ToDo list. You can add goals and remove them.
To try and get my head around it I thought I'd try and do the 'delete' code myself. I want to know how I can make my method work:
export default function App() {
const [courseGoals, setCourseGoals] = useState([]);
function addGoalHandler(goalTitle) {
setCourseGoals([goalTitle, ...courseGoals]);
}
function removeGoalHandler(index) {
console.log(index)
setCourseGoals(courseGoals.splice(index, 1));
}
return (
<View style={styles.screen}>
<GoalInput onAddGoal={addGoalHandler}/>
<ScrollView>
{courseGoals.map((goal, index) => <GoalItem onDelete={() => removeGoalHandler(index)} title={goal} key={index}/>)}
</ScrollView>
</View>
);
}
What I thought would happen on line 10 is that the courseGoals array would be set to courseGoals minus the spliced element. Instead, I have discovered that splice actually returns the deleted item. How can I fix this? Is there no way to just get courseGoals.splice() to return the array without the spliced element?
You can do it by using slice:
function removeGoalHandler(index) {
console.log(index);
setCourseGoals([...courseGoals.slice(0, index), ...courseGoals.slice(index + 1)]);
}
or with splice:
function removeGoalHandler(index) {
console.log(index);
const courseGoalsCopy = [...courseGoals];
courseGoalsCopy.splice(index, 1);
setCourseGoals(courseGoalsCopy);
}
I have a pretty simple custom component: two select lists with buttons to move the options from the available (left) list to the selected (right) list. Naturally, the moved element should no longer show up on the list it was moved from. Though both buttons successfully add the element to the target, it doesn't remove from the source, because when I pass the reduced array of items to setState, the render still returns with the original list.
EDIT posting most of the component code for clarification. The problem methods are the addItems and removeItems, where setState is called. In both cases, whichever array property is being reduced/filtered is the one not updating; the one being added to always updates properly.
... imports
interface JoinedListState {
availableItems: ListItem[]
selectedItems: ListItem[]
}
export class JoinedList extends React.Component<JoinedListState, any>{
// Create new arrays of the proper available and selected then set the new
// state
private addItems(newItems: ListItem[]) {
let oldSelected = this.props.selectedItems;
oldSelected.push.apply(oldSelected, newItems);
let newSelected = oldSelected.sort((a, b) => {
let nameA = a.value.toUpperCase();
let nameB = b.value.toUpperCase();
if (nameA < nameB) {
return -1
}
return 1
});
let newAvailable = this.props.availableItems
.slice(0) // updated on recommendation of Sasha Kos
.filter((item) => {
return newItems.findIndex(i => i.id == item.id) == -1
});
this.setState({
availableItems: newAvailable,
selectedItems: newSelected
});
}
// Create new arrays of the proper available and selected then set the
//new state
private removeItems(removedItems: ListItem[]) {
.. same approach as addItems
let newSelected = this.props.selectedItems.filter((item) => {
// return only the items whose id does not exist on the newly
//removed items list
return removedItems.findIndex(i => i.id == item.id) == -1
})
this.setState({
availableItems: newAvailable,
selectedItems: newSelected
})
}
// Get the selected items by querying the DOM and send them to function
// to update state
addSelected(event: React.FormEvent<HTMLButtonElement>) {
// Code removed for brevity: uses the event object to find the
//selected objects and builds a ListItem array called 'selected'
//to pass to addItems
this.addItems(selected)
}
removeSelected(event: React.FormEvent<HTMLButtonElement>) {
// Code removed for brevity: uses the event object to find the
//selected objects and builds a ListItem array called 'selected'
//to pass to addItems
this.removeItems(selected)
}
render() {
let aItems = this.renderOptionList(this.props.availableItems),
sItems = this.renderOptionList(this.props.selectedItems);
return (
<div className='joined-list-container'>
<select key='available_list' className='available-list form-
control' multiple>
{aItems}
</select>
<span className='button-container'>
<button key='button1' className='btn btn-success'
onClick={this.addSelected.bind(this)}>
<span className='glyphicon glyphicon-chevron-right'>
</span>
</button>
<button key='button2' className='btn btn-danger'
onClick={this.removeSelected.bind(this)}>
<span className='glyphicon glyphicon-chevron-left'>
</span>
</button>
</span>
<select key='selected_list' className='selected-list form-
control' multiple>
{sItems}
</select>
</div>
)
}
renderOptionList(items: ListItem[]) {
return items.map((item, idx) => {
let key = `${item.value}_${idx}`
return (
<option value={item.id} key={key}>{item.value}</option>
)
})
}
}
(Sorry for any flawed formatting, posting was tricky)
When this kicks off the new render, the selectedItems list is properly updated with the new item(s), but the availableItems is always the original array (yes I've ensured that the newAvailable array is properly filtered down), and even when I try
this.setState({
availableItems: [],
selectedItems: newSelected
})
I get the original availableItems array on the next render.
Is there some nuance to returning similar-but-shorter arrays to state via setState? I can't find anything referencing this behavior, and not sure what I'm missing.
Thanks
This is the issue:
let oldSelected = this.props.selectedItems;
oldSelected.push.apply(oldSelected, newItems);
You are updating this.props.selectedItems here, but for availableItems:
let newAvailable = this.props.availableItems
.slice(0) // updated on recommendation of Sasha Kos
.filter((item) => {
return newItems.findIndex(i => i.id == item.id) == -1
});
Here, you do not directly update this.props.availableItems. The reason this matters is that when you call setState and render is triggered these methods:
let aItems = this.renderOptionList(this.props.availableItems),
sItems = this.renderOptionList(this.props.selectedItems);
are using this.props to return arrays, NOT this.state. this.props.selectedItems has changed, and thus returns a different array, while this.props.availableItems has not changed.
tl;dr - use this.state instead of this.props when passing the arrays to your renderOptionList method.
According to mozilla docs Array.prototype.filter should create new array, but described symptoms says that you just get 2 references to one array so there is no rerender. So please try this
let newAvailable = this.props.availableItems
.slice(0) /* clones your array */
.filter((item) => {
return newItems.findIndex(i => i.id == item.id) == -1
});
this.setState({
availableItems: newAvailable,
selectedItems: newSelected
});
Currently in React, I am using array.map(function(text,index){}) to iterate through an array. But, how am I going to iterate through two arrays simultaneously using map?
EDIT
var sentenceList = sentences.map(function(text,index){
return <ListGroupItem key={index}>{text}</ListGroupItem>;
})
return (
<div>
<ListGroup>
{sentenceList}
</ListGrouup>
</div>
);
Like, in this I want icons to be prepended with every iteration. And I'm planning to have those icons in another array. So, thats why iterate two arrays.
If at all possible, I would recommend storing the text alongside the images in an array of objects, eg:
const objects = [{text: 'abc', image: '/img.png' }, /* others */];
that way you can just iterate through the array and select both members at the same time, for example:
objects.map(item => (<Component icon={item.image} text={item.text} />) )
If this isn't possible then just map over one array and access the second array's members via the current index:
sentences.map((text, index) => {
const image = images[index];
return (<Component icon={image} text={text} />);
});
Are the both arrays of same length? You can do something like below if your intention is to combine both in some way.
array.map(function(text,index){
return text + ' ' + array2[index]
})
In your case:
var sentenceList = sentences.map(function(text,index){
return <ListGroupItem key={index}>{text} <img src={icons[index]} /i> </ListGroupItem>;
})
return (
<div>
<ListGroup>
{sentenceList}
</ListGrouup>
</div>
);
Notice, How Icon src is being assigned. The idea is that access icons array with the same index to get a corresponding icon.
You can't do this with built-in Array.prototype methods, but you can use something like this:
function map2(arr1, arr2, func) {
return arr1.map(
(el, i) => { return func(el, arr2[i]); }
);
}
(Of course, arr1 and arr2 are expected to have the same length)
Generally, what you're looking for is a zip function, such as the one that lodash provides. Much like a real zipper, it combines two things of the same length into one:
const zipped = _.zip(sentences, icons);
return (
<div>
<ListGroup>
{zipped.map(([sentence, icon], index) => (
<ListGroupItem key={index}><Icon icon={icon} /> {text}</ListGroupItem>;
))}
</ListGroup>
</div>
);
Note, this is doing more iterations than are technically needed. If performance is an issue, you may want a solution that's a bit smart (not really in scope for your question though).