React setState not updating reduced array - javascript

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
});

Related

Remove element from useState array by index

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"

filtering out an array of items setting state

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));
};

How do i update value of a key in an object inside an Array in React?

I have a dynamic list of items a user adds. I want to avoid duplicating when a user adds an item already present in the list. My list looks like
itemList = [ {itemId:1, name:"x", quantity:5}, {itemId:4, name:"y", quantity:2}]
so now if a user adds item x with quantity 2 i want the object with item x to update the quantity to 7 rather than adding a whole new object.
I am using find() method to get the item already present and storing it to a variable, itemObj is the item the user recently added.
let alrItem = state.itemList.find(
(e) => e.itemId === itemObj.itemId
);
let newItem = alrItem;
newItem.quantity += itemObj.quantity;
How do I merge this newItem to the itemList so that it just updates the quantity of that specific item?
What you are doing is finding the object in the itemList array and then mutating the state directly. State should not be mutated directly.
Instead of using .find() method, use the .map() method to iterate over the array and update the quantity of the item that matches with the id of the new item.
let updatedItemList = state.itemList.map((item) => {
if (item.itemId === itemObj.itemId) {
return { ...item, quantity: item.quantity + itemObj.quantity };
}
return item;
});
// update the state
setItemList(updatedItemList);
Note that above code will not do anything if the item isn't already present in the itemList. Ideally, you should also handle the case where the new item isn't already present in the itemList. In this case, you should just add the new item in the itemList.
To handle this case, all you need is a extra variable that can be used to know whether the if condition inside the .map() method evaluated to true or not.
let exists = false;
let updatedItemList = state.itemList.map((item) => {
if (item.itemId === itemObj.itemId) {
exists = true;
return { ...item, quantity: item.quantity + itemObj.quantity };
}
return item;
});
// if the item isn't present in the list, add it in the "updatedItemList"
if (!exists) {
updatedItemList.push(itemObj);
}
// update the state
setItemList(updatedItemList);
let itemList = [ {itemId:1, name:"x", quantity:5}, {itemId:4, name:"y", quantity:2}]
let itemObj = {itemId:1, name:"x", quantity:2}
const target = itemList.find(element =>
element.itemId === itemObj.itemId
);
if (target) {
target.quantity = target.quantity + itemObj.quantity;
} else {
itemList.push(itemObj);
}
console.log('itemList: ' + JSON.stringify(itemList));
OUTPUT:
itemList: [{"itemId":1,"name":"x","quantity":7},{"itemId":4,"name":"y","quantity":2}]

React manage array multidimensional by its index

I have a problem how to update(in this case Add or Remove Cargo) my multidimensional array based on the selected index. The parent array has been successful, but I am confused for the child how to process the data when clicking the Add Cargo button to add cargo and the Remove Cargo button to delete all cargo based on the selected index.
Please help. This is my codesandbox code
Sorry for the view, maybe is not good enough
You will need to provide index to the functions to add and/or delete cargos. In the function update the nested fields by mapping over the inputFields. While calling the handleAddCargo, pass the index and while calling handleRemoveCargo, pass the index as well as finalIndex which is the cargo array index.
const handleAddCargo = (parentFiledIndex) => {
const updatedInputFields = inputFields.map((item, i) => {
if(parentFiledIndex === i){
return {...item, cargo: item.cargo.concat({
cargoId: '',
cargoDescription: "",
cargoHsCode: ""
})}
}else{
return item
}
});
setInputFields(updatedInputFields);
console.log("add by its index cargo here");
};
const handleRemoveCargo = (parentFiledIndex, cargoIndex) => {
const updatedInputFields = inputFields.map((item, i) => {
if(parentFiledIndex === i){
return {...item, cargo: item.cargo.filter((cargo, c) => c !== cargoIndex)}
}else{
return item
}
});
setInputFields(updatedInputFields);
console.log("remove by its index cargo here");
};
updated working solution is here
https://codesandbox.io/s/reverent-bose-c2nkk
quick note -
try not to use array indexes while rendering lists. For eg, use some library to generate unique id

How to search a value in an array inside another array

I have a problem of find a value in an array inside another array, and use the result to setState()
This is the initialState:
this.state =
{
initialStudents:[
{name:"str1",tags;["str","str",...],...},
{name:"str2",tags;["str","str",...],...},
...
],
students: [
{name:"str1",tags;["str","str",...],...},
{name:"str2",tags;["str","str",...],...},
...
]
}
The code i use to find the tags:
findTag = (tags, target) => {
tags.filter(tag => {
return tag.toLowerCase().search(target.toLowerCase()) !== >-1;
});
};
filterTag = e => {
let updatedList = this.state.initialStudents;
updatedList = updatedList.filter(student => {
return this.findTag(student.tags, e.target.value);
});
this.setState({ students: updatedList });
};
The filterTag does not update the students state
To solve your problem, I made a few edits and put them all in this working codesandbox example.
First, I changed your findTag function to something like this:
// pass in the tags from the student, and the target tag you're searching for.
// -> return true if 1 or more matching tag, false otherwise
findTag = (tags, targetTag) => {
// make sure you return something!
return tags.filter(tag => {
// check if current tag in arr matches target tag (case insensitive)
return tag.toLowerCase() === targetTag.toLowerCase();
}).length > 0; // check if there's 1 or more matching tag
};
Next, I updated the filterTag function in a few ways:
Immutably copy this.state.initialStudents into the local updatedList array. This is necessary so you don't mess up the current state before running this.setState!
Pass the value of the input via this.state.filterTag instead of e.target.value. This way, you'd update the filter when you click the button instead of on every time you press a key.
Here's how these changes look:
filterTag = e => {
// immutably copy initial student data
let updatedList = this.state.initialStudents
.map(student => ({
name: student.name,
tags: [...student.tags]
}))
// remove students w/out filter tag
.filter(student => {
return this.findTag(student.tags, this.state.filterTag);
});
// update state with new student list
this.setState({ students: updatedList });
};
A few other improvements I made:
Instead of manually setting data in initialStudents and students, I made them immutably copy the same data set from the const initialStudents data set. This could be done in the componentDidMount lifecycle method if you're fetching students from a database.
I fixed your student object declarations - you put tags;["str"...] which is invalid - the semicolon ; should be a normal colon :
I changed some "str" values to "str2" to make them unique between students
Let me know if you have questions about the codesandbox or anything else :D Hope it helps!

Categories

Resources