如何在对象数组中使用useState更新状态?

19

我在使用React的useState钩子时遇到了一些问题。我有一个待办事项清单,其中包含一个复选框按钮,我希望将具有与“点击”复选框按钮的id相同的id的“完成”属性更新为“true”。如果我在控制台中记录我的“toggleDone”函数,则返回正确的id。但是我不知道如何更新正确的属性。

当前状态:

const App = () => {

  const [state, setState] = useState({
    todos: 
    [
        {
          id: 1,
          title: 'take out trash',
          done: false
        },
        {
          id: 2,
          title: 'wife to dinner',
          done: false
        },
        {
          id: 3,
          title: 'make react app',
          done: false
        },
    ]
  })

  const toggleDone = (id) => {
    console.log(id);
}

  return (
    <div className="App">
        <Todos todos={state.todos} toggleDone={toggleDone}/>
    </div>
  );
}

我希望的最新状态:

const App = () => {

  const [state, setState] = useState({
    todos: 
    [
        {
          id: 1,
          title: 'take out trash',
          done: false
        },
        {
          id: 2,
          title: 'wife to dinner',
          done: false
        },
        {
          id: 3,
          title: 'make react app',
          done: true // if I checked this checkbox.
        },
    ]
  })

如果您提供您尝试设置状态的方式,将会很有帮助。 - tsfahmad
你需要使用修改后的状态调用 setState()。你尝试过什么了吗?如果有,结果如何?如果你不知道如何开始,可以查看 map() 函数。 - Code-Apprentice
这回答解决了你的问题吗?在ReactJS中更新数组中对象的最佳方法是什么? - Emile Bergeron
此外,使用钩子时无需将数组嵌套在对象中。您可以多次调用 useState 来单独管理不同的状态值。 - Emile Bergeron
7个回答

42

你可以安全地使用JavaScript的数组map功能,因为它不会修改现有状态。React不喜欢状态被修改,而且它返回一个新数组。这个过程是在状态数组上循环并找到正确的id。更新done值。然后使用更新后的列表设置状态。

const toggleDone = (id) => {
  console.log(id);

  // loop over the todos list and find the provided id.
  let updatedList = state.todos.map(item => 
    {
      if (item.id == id){
        return {...item, done: !item.done}; //gets everything that was already in item, and updates "done"
      }
      return item; // else return unmodified item 
    });

  setState({todos: updatedList}); // set state to new object with updated list
}

编辑:更新代码以切换item.done,而不是将其设置为true。


1
我想指出@bravemaster回答中的一些内容。他们使用{...state, todos: [...state.todos]}来扩展状态,这是一个好的做法。使用我的解决方案,如果您在状态中包含除“todos”以外的任何其他内容,则会在“setState”操作中丢失它(现在它可以正常工作,因为状态中只有todos对象)。保留所有其他状态并同时更新todos的方法是setState({...state, todos: updatedList}); - D. Smith
创建答案。我一直在使用这个模式。但是我对成本有一个问题。使用地图循环在这个模式上施加了线性成本,使其在大型数组中不起作用。是否有另一种模式可以利用数组的索引特性? - undefined
在这种情况下,state todos是一个数组,因此线性搜索是一个O(n)的操作。我不知道这种情况下是否有其他特定的模式,但也许我们可以尝试其他方法。如果我们假设id是递增的,就像例子中一样,我们可以将第一个和最后一个作为边界,实现二分搜索,或者我认为更简单的方法是将todos数组转换为一个对象,其中id作为键,这样我们就可以直接跳转到需要修改的那个。无论哪种方式,记住我们需要制作一个副本而不是修改原始状态,这也是map函数为我们做的。 - undefined

9

D. Smith的回答很好,但可以重构得更加声明性,如下所示。

const toggleDone = (id) => {
 console.log(id);
 setState(state => {
     // loop over the todos list and find the provided id.
     return state.todos.map(item => {
         //gets everything that was already in item, and updates "done" 
         //else returns unmodified item
         return item.id === id ? {...item, done: !item.done} : item
     })
 }); // set state to new object with updated list
}

这里的问题在于,state不再是一个包含数组的todos属性的对象,而是直接包含该数组。相反,您可以在返回语句中使用扩展语法,例如 return {...state, todos: state.todos.map( - Neil Haskins

9
你需要像这样使用 展开运算符
const toggleDone = (id) => {
    let newState = [...state];
    newState[index].done = true;
    setState(newState])
}

5
这也会改变当前状态,这在 React 中是一种反模式。 - Emile Bergeron
@EmileBergeron 正在将状态首先复制到 newState 中。 - Facundo Colombier
1
@FacundoColombier 这只是一个浅复制。数组内部的对象与原始状态中的对象相同。 - Emile Bergeron
使用扩展运算符 [...state] 实际上是在创建一个副本,而不是与原始状态链接,因此不会修改任何状态,这是一种“反模式”。 - Facundo Colombier
Emile 在这里是正确的。[...state] 只会进行浅拷贝。你需要再进行第二次拷贝,像这样:newState[index] = {... newState[index], done: true} - JohnFlux

6

需要类似于D. Smith's answer的内容,但更加简洁:

const toggleDone = (id) => {

  setState(prevState => {
            // Loop over your list
            return prevState.map((item) => {
                // Check for the item with the specified id and update it
                return item.id === id ? {...item, done: !item.done} : item
            })
        })
}

这似乎与Sam Kingston的答案相同。 - Emile Bergeron
setState(prevState => prevState.map(item => item.id ? { ...item, done: !item.done} : item)) - JohnFlux

4
const toggleDone = (id) => {
    console.log(id);
    // copy old state
    const newState = {...state, todos: [...state.todos]};
    // change value
    const matchingIndex = newState.todos.findIndex((item) => item.id == id);
    if (matchingIndex !== -1) {
       newState.todos[matchingIndex] = {
           ...newState.todos[matchingIndex], 
           done: !newState.todos[matchingIndex].done 
       }
    }
    // set new state
    setState(newState);
}

2
所有的答案都很好,但我会这样做。
setState(prevState => {
    ...prevState,
    todos: [...prevState.todos, newObj]
})

这将安全地更新状态。同时,数据完整性将得到保持。这也将解决更新时的数据一致性问题。
如果您想执行任何条件,请按照以下方式操作。
setState(prevState => {
    if(condition){
        return {
            ...prevState,
            todos: [...prevState.todos, newObj]
        }
    }else{
        return prevState
    }
})

1
在哪里检查条件,例如当条件为真时更新嵌套项,否则保持不变。 - Jamal Ud Din

-1
我会使用useState仅创建todos数组,而不是另一个状态。关键在于创建todos数组的副本、更新该副本并将其设置为新数组。
以下是工作示例: https://codesandbox.io/s/competent-bogdan-kn22e?file=/src/App.js
const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      title: "take out trash",
      done: false
    },
    {
      id: 2,
      title: "wife to dinner",
      done: false
    },
    {
      id: 3,
      title: "make react app",
      done: false
    }
  ]);

  const toggleDone = (e, item) => {
    const indexToUpdate = todos.findIndex((todo) => todo.id === item.id);
    const updatedTodos = [...todos]; // creates a copy of the array

    updatedTodos[indexToUpdate].done = !item.done;
    setTodos(updatedTodos);
  };

你和第一个回复者遇到了相同的问题。你需要创建一个副本,如下所示: updatedTodos[indexToUpdate].done = {...item, done: !item.done;} - JohnFlux

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