使用 hooks (useState) 从数组中删除对象

86

我有一个对象数组。我需要添加一个函数来从数组中删除一个对象,而不使用“this”关键字。

我尝试使用updateList(list.slice(list.indexOf(e.target.name, 1)))。这会将除了数组中的最后一项之外的所有内容都删除,但我不确定原因是什么。

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

预期行为:点击的项目将从列表中删除。
实际行为:整个列表被删除,除了数组中的最后一个项目。


首先,span 标签上没有 name 属性。做一些基本的调试,看看 e.target.name 是什么。接着... 阅读关于 slice()indexOf() 工作原理的文档。该方法存在多个问题。 - charlietfl
你说得对,span 标签上没有名称。这就是为什么名称是指从数组映射的项目。你有读过这篇文章吗? - collinswade408
不,在您使用 onClick={handleRemoveItem} 的上下文中它并不起作用。您是否在函数内部进行了检查? - charlietfl
@collinswade408,请看下面的解决方案。这应该能让你了解如何完成你的解决方案以及为什么你的代码不起作用。 - Chris Ngo
8个回答

106
首先,具有点击事件的元素需要具有属性,否则在中找不到名称。 话虽如此,保留用于表单元素(input、select等)。 因此,要实际使用属性,必须使用。
另外,由于您有一个对象数组,因此使用将不会有效,因为它正在查找<字符串>,而您正在迭代对象。这就像是在<{}, {}, {}>中查找“dog”。
最后,返回一个新数组,从您传递给它的索引开始。 因此,如果您点击了最后一个项目,则只会返回最后一个项目。
相反,尝试使用<.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(list.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);

一个<span>不能有name属性或者正确地使用。 - Will Jenkins
@WillJenkins 当然可以,只是你无法通过 event.target.name 看到它。 - Chris Ngo
请检查规范,<span>不能有name属性。https://www.w3.org/TR/REC-html40/index/attributes.html - Will Jenkins
1
如果两个元素具有相同的名称值怎么办?它将删除这两个元素。@ChrisNgo - Pep

37
你可以使用Array.filter在一行代码中完成这个操作。
const handleRemoveItem = name => {
    updateList(list.filter(item => item.name !== name))
}

Eta: 你还需要在 onClick 处理程序中传递你的项目名称:

{list.map(item => {
    return ( 
        <>
        <span onClick={() =>handleRemoveItem(item.name)}>x </span>
        <span>{item.name}</span>
        </>
    )}

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

)

1
这个例子结束后,临时变量不会留在内存中吗?对于大型工作负载来说,这并不好。 - Ian Smith
2
@IanSmith 有什么建议吗? - Kevin
1
JavaScript会在值超出作用域时收集垃圾。请参见https://dev59.com/21wZ5IYBdhLWcg3wPOP2 - isherwood

2

我认为这是目前最好的答案,只有一点小改进

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

我们不需要给它一个名称属性,只需将其发送到处理函数即可。


2
有两个小错误,需要修正: 1)const handleRemoveItem = (name) => { ... // 等等2)onClick={()=>{handleRemoveItem(item.name)}} - okram

1

冗余的一行代码——不建议使用,因为很难测试、输入、扩展、重复和理解。

 <button onClick={() => setList(list.slice(item.id - 1))}

没有导出的版本:

 const handleDeleteItem = id => {
     const remainingItems = list.slice(id - 1)
     setList(remainingItems);
}

然而,我建议您通过在另一个文件中使用辅助函数来不同地扩展您的逻辑结构。

考虑到这一点,我为过滤器和切片分别制作了一个示例。在这种特定的用例中,我个人喜欢使用切片选项,因为它使得推理变得更加容易。显然,如果需要扩展(请参见引用),在更大的列表上,它也略微更具性能优势。

如果使用切片,请始终使用切片,而不是splice,除非您有充分的理由不这样做,因为它符合函数式风格(纯函数没有副作用)。

// 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)

示例(导入选项过滤器和切片选项)

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

参考文献:


这些链接证明了阅读它们是有趣的! - Harrison

1

我认为这段代码可以完成任务。

let targetIndex = list.findIndex((each) => {each.name == e.target.name});
list.splice(targetIndex-1, 1);

我们需要检查对象内的名称值,因此使用findIndex。然后从目标索引开始剪切对象到目标索引后的1个数组。

Codepen

从您的评论中可以看出,您的问题来自另一部分。

更改此视图部分

    return ( 
        <>
        <span onClick={() => handleRemoveItem(item) }>x </span>
        <span>{item.name}</span>
        </>
    )}

更改函数 handleRemoveItem 的格式

const handleRemoveItem = item => {
    list.splice(list.indexOf(item)-1, 1)
    updateList(list);
}

2
最好不要改变原始数组。 - charlietfl
谢谢您的快速回答,但是这会返回一个 TypeScript 错误:Argument of type '(each: { name: string; }) => void' is not assignable to parameter of type '(value: {name: string; }, index: number, obj: { name: string;}[]) => boolean'. - collinswade408
你可以检查一下代码笔。我认为问题在另一个部分,而不是这个部分。 - William Gunawan
现在的根本问题是 e.target.name 未定义。 - charlietfl
1
完成。您可以再次检查。希望您的问题已经解决! - William Gunawan
显示剩余2条评论

0
使用这种模式,数组不会跳跃,而是我们取先前的数据并创建新数据并返回它。
 const [list, updateList] = useState([
    { name: "ItemOne" },
    { name: "ItemTwo" },
    { name: "ItemThree" }
  ]);

     updateList((prev) => {
                return [
                    ...prev.filter((item, i) => item.name !== 'ItemTwo')
                ]
            })

为了减少代码行数,你可以像这样删除 return 语句: updateList((prev) => ([...prev.filter((item) => item.name !== 'ItemTwo')])) - Harrison

-1

这是因为slicesplice都返回一个包含已移除元素的数组。

您需要对数组应用splice,然后使用钩子提供的方法更新状态

const handleRemoveItem = e => {
    const newArr = [...list];
    newArr.splice(newArr.findIndex(item => item.name === e.target.name), 1)
    updateList(newArr)
}

谢谢您的快速回复。不幸的是,上述方法并没有起作用,所以我尝试了以下代码:const newArr= list.splice(list.indexOf(e.target.name, 1)) updateList(newArr)但这给我带来了与我的原始帖子相同的问题。 :( - collinswade408
这是因为splice方法仍然返回一个被移除元素的数组,即使你将其存储在一个变量中。 - Morphyish
你不能这样在对象数组上使用 indexOf() - charlietfl
是的,我刚刚进行了一次编辑,我复制/粘贴了代码中的这部分内容,但没有注意数据结构。 - Morphyish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接