Reputation: 925
I have an array of objects. I need to add a function to remove an object from my array without using the "this" keyword.
I tried using updateList(list.slice(list.indexOf(e.target.name, 1)))
. This removes everything but the last item in the array and I'm not certain why.
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }]
const [list, updateList] = useState(defaultList);
const handleRemoveItem = e => {
updateList(list.slice(list.indexOf(e.target.name, 1)))
}
return (
{list.map(item => {
return (
<>
<span onClick={handleRemoveItem}>x </span>
<span>{item.name}</span>
</>
)}
}
)
Expected behaviour: The clicked item will be removed from the list.
Actual behaviour: The entire list gets removed, minus the last item in the array.
Upvotes: 92
Views: 200273
Reputation: 15698
First of all, the span
element with the click event needs to have a name
property otherwise, there will be no name to find within the e.target
. With that said, e.target.name
is reserved for form elements (input, select, etc). So to actually tap into the name property you'll have to use e.target.getAttribute("name")
Additionally, because you have an array of objects, it would not be effective to use list.indexOf(e.target.name)
since that is looking for a string
when you are iterating over objects. That's like saying find "dog" within [{}, {}, {}]
Lastly, array.slice()
returns a new array starting with the item at the index you passed to it. So if you clicked the last-item, you would only be getting back the last item.
Try something like this instead using .filter()
: codesandbox
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];
const [list, updateList] = useState(defaultList);
const handleRemoveItem = (e) => {
const name = e.target.getAttribute("name")
updateList(l => l.filter(item => item.name !== name));
};
return (
<div>
{list.map(item => {
return (
<>
<span name={item.name} onClick={handleRemoveItem}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Upvotes: 111
Reputation: 11
Using this pattern, the array does not jump, but we take the previous data and create new data and return it.
const [list, updateList] = useState([
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
]);
updateList((prev) => {
return [
...prev.filter((item, i) => item.name !== 'ItemTwo')
]
})
Upvotes: 0
Reputation: 31
Redundant one liner - would not recommend as hard to test / type / expand / repeat / reason with
<button onClick={() => setList(list.slice(item.id - 1))}
A version without exports:
const handleDeleteItem = id => {
const remainingItems = list.slice(id - 1)
setList(remainingItems);
}
However I would consider expanding the structure of your logic differently by using helper functions in another file.
With that in mind, I made one example for filter and another for slice. I personally like the slice option in this particular use-case as it makes it easy to reason with. Apparently, it is also slightly more performant on larger lists if scaling (see references).
If using slice, always use slice not splice unless you have good reason not to do so as it adheres to a functional style (pure functions with no side effects)
// use slice instead of splice (slice creates a shallow copy, i.e., 'mutates' )
export const excludeItemFromArray = (idx, array) => array.slice(idx-1)
// alternatively, you could use filter (also a shallow copy)
export const filterItemFromArray = (idx, array) => array.filter(item => item.idx !== idx)
Example (with both options filter and slice options as imports)
import {excludeItemFromArray, filterItemFromArray} from 'utils/arrayHelpers.js'
const exampleList = [
{ id: 1, name: "ItemOne" },
{ id: 2, name: "ItemTwo" },
{ id: 3, name: "ItemThree" }
]
const [list, setList] = useState(exampleList);
const handleDeleteItem = id => {
//excluding the item (returning mutated list with excluded item)
const remainingItems = excludeItemFromArray(id, list)
//alternatively, filter item (returning mutated list with filtered out item)
const remainingItems = filterItemFromArray(id, list)
// updating the list state
setList(remainingItems);
}
return (
{list.map((item) => (
<div key={item.id}>
<button onClick={() => handleDeleteItem(item.id)}>x</button>
<span>{item.name}</span>
</div>
))}
)
References:
Don't use index keys in maps: https://robinpokorny.com/blog/index-as-a-key-is-an-anti-pattern/
Performance of slice vs filter: https://medium.com/@justintulk/javascript-performance-array-slice-vs-array-filter-4573d726aacb
Slice documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Functional programming style: https://blog.logrocket.com/fundamentals-functional-programming-react/#:~:text=Functional%20programming%20codes%20are%20meant,computations%20are%20called%20side%20effects.
Upvotes: 2
Reputation: 1238
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
]
const [list, updateList] = useState(defaultList);
const handleRemoveItem = idx => {
// assigning the list to temp variable
const temp = [...list];
// removing the element using splice
temp.splice(idx, 1);
// updating the list
updateList(temp);
}
return (
{list.map((item, idx) => (
<div key={idx}>
<button onClick={() => handleRemoveItem(idx)}>x </button>
<span>{item.name}</span>
</div>
))}
)
Upvotes: 28
Reputation: 21
Small improvement in my opinion to the best answer so far
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];
const [list, updateList] = useState(defaultList);
const handleRemoveItem = (item) => {
updateList(list.filter(item => item.name !== name));
};
return (
<div>
{list.map(item => {
return (
<>
<span onClick={()=>{handleRemoveItem(item)}}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Instead of giving a name attribute we just send it to the handle function
Upvotes: 2
Reputation: 750
I think this code will do
let targetIndex = list.findIndex((each) => {each.name == e.target.name});
list.splice(targetIndex-1, 1);
We need to check name value inside object so use findIndex instead. then cut the object start from target index to 1 array after target index.
From your comment your problem came from another part.
Change this view section
return (
<>
<span onClick={() => handleRemoveItem(item) }>x </span>
<span>{item.name}</span>
</>
)}
change function handleRemoveItem format
const handleRemoveItem = item => {
list.splice(list.indexOf(item)-1, 1)
updateList(list);
}
Upvotes: 1
Reputation: 9887
You can use Array.filter to do this in a one-liner:
const handleRemoveItem = name => {
updateList(list.filter(item => item.name !== name))
}
Eta: you'll also need to pass the name of your item in your onClick handler:
{list.map(item => {
return (
<>
<span onClick={() =>handleRemoveItem(item.name)}>x </span>
<span>{item.name}</span>
</>
)}
Upvotes: 38
Reputation: 4072
This is because both slice and splice return an array containing the removed elements.
You need to apply a splice
to the array, and then update the state using the method provided by the hook
const handleRemoveItem = e => {
const newArr = [...list];
newArr.splice(newArr.findIndex(item => item.name === e.target.name), 1)
updateList(newArr)
}
Upvotes: -1