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"
Related
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));
};
export default function ShoppingCart() {
const classes = useStyle();
const {
productsList, filteredProductsList, setFilteredProductsList, setProductsList,
} = useContext(productsContext);
const [awaitingPaymentList, setAwaitingPaymentList] = useState([]);
const [addedToCartList, setAddedToCartList] = useState([]);
const addToCartHandler = useCallback((itemId) => {
const awaitingPaymentListIds = awaitingPaymentList.map((item) => item.id);
const isInAwaitingPaymentList = awaitingPaymentListIds.includes(itemId);
isInAwaitingPaymentList ? setAddedToCartList([...addedToCartList, addedToCartList.push(awaitingPaymentList[awaitingPaymentList.findIndex((item) => item.id === itemId)])]) : setAddedToCartList([...addedToCartList]);
isInAwaitingPaymentList
? setAwaitingPaymentList(awaitingPaymentList.splice(awaitingPaymentList.findIndex((item) => item.id === itemId), 1))
: setAwaitingPaymentList([...awaitingPaymentList ])
setProductsList(awaitingPaymentList);
}, [addedToCartList, awaitingPaymentList, setProductsList]);
useEffect(() => {
setFilteredProductsList(
productsList.filter((product) => product.status === 'AWAITING_PAYMENT'),
);
}, [productsList, setFilteredProductsList, setFilteredProductsList.length]);
useEffect(() => {
setAwaitingPaymentList(filteredProductsList);
}, [filteredProductsList]);
I manage to delete the item from awaitingPaymentList and to add it into addedToCartList but looks like I am doing something wrong because it is adding the object, but the previous ones are replaced with numbers :). On the first click, the array is with one object inside with all data, but after each followed click is something like this => [1,2,3, {}].
When I console log addedToCartList outside addToCartHandler function it is showing an array: [1] :)))
Since there is some code I hope I am not going to receive a lot of negative comments like last time. And if it's possible, to give me a clue how to make it for all items to be transferred at once, because there will be a button to add all. Thank you for your time.
I think this line of code is causing issue:
isInAwaitingPaymentList
? setAddedToCartList([
...addedToCartList,
addedToCartList.push(
awaitingPaymentList[
awaitingPaymentList.findIndex((item) => item.id === itemId)
]
)
])
: setAddedToCartList([...addedToCartList]);
array.prototype.push returns the new length of the array that you are pushing into, this is likely where the incrementing element values are coming from. The push is also a state mutation.
It is not really clear what you want this code to do, but I think the push is unnecessary. Perhaps you meant to just append the last element into the new array you are building.
isInAwaitingPaymentList
? setAddedToCartList([
...addedToCartList, // <-- copy state
awaitingPaymentList[ // <-- add new element at end
awaitingPaymentList.findIndex((item) => item.id === itemId)
]
])
: setAddedToCartList([...addedToCartList]);
Suggestion
If you simply want to move an element from one array to another then find it in the first, then filter it from the first, and copy to the second if it was found & filtered.
const itemToMove = awaitingPaymentList.find(item => item.id === itemId);
setAwaitingPaymentList(list => list.filter(item => item.id !== itemId));
itemToMove && setAddedToCartList(list => [...list, { ...itemToMove }])
I'm creating a football betting app, where I'd like to pick a winner/loser/draw from a match, and then store that in a list of selected bets.
What I have so far
For each match, you can select a winner of either team or a draw. The getSelection method is passed down as props to the onClick handler of each button.
The getSelection method then adds the value of the button click to an array, like so:
getSelection = (val: object) => {
this.setState(
{bets: [...this.state.bets, val]},
() => console.log(this.state.bets, "bets")
);
};
I want only to be able to make a single selection for each match, represented visually like so:
Codesandbox.
This solution is updating the Match Results based on the User's selection.
SINGLE MATCH WILL HAVE SINGLE RESULT, i.e RESULTS WILL OVERWRITE
Also to keep track of Match Number, I have used matchNumber as an index.
Your getSelection will look something like this.
getSelection = (val: object, matchNumber: number) => {
// 1. Make a shallow copy of the items
let bets = [...this.state.bets];
// 2. Make a shallow copy of the item you want to mutate
let bet = { ...bets[matchNumber] };
// 3. Replace the property you're intested in
bet = val;
// 4. Put it back into our array. N.B. we *are* mutating the array here, but that's why we made a copy first
bets[matchNumber] = bet;
// 5. Set the state to our new copy
this.setState({ bets }, () => console.log(this.state.bets));
Update the background:
<button
style={{ background: winner == home.name ? "lightblue" : "white" }}
onClick={() => this.props.getSelection(home, matchNumber)}
>
{home.name}
</button>
<button
style={{ background: winner == draw.name ? "lightblue" : "white" }}
onClick={() => this.props.getSelection(draw, matchNumber)}
>
{draw.name}
</button>
<button
style={{ background: winner == away.name ? "lightblue" : "white" }}
onClick={() => this.props.getSelection(away, matchNumber)}
>
{away.name}
</button>
Check this working solution. https://codesandbox.io/s/9lpnvx188y
Are you saying you should only be able to add a single possibility once?
Then this might work:
getSelection = (val: object) => {
this.setState( {
bets: [...this.state.bets, val].filter((value, index, self) => { return self.indexOf(value) === index; })
},
() => console.log(this.state.bets, "bets")
);
};
(That is, take the entire array, add the new value, filter so you only get the distinct values, and store it in bets).
ala: https://codesandbox.io/s/x2o5m95oxw
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.
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
});