为什么useState不会触发重新渲染?

227

我初始化了一个状态,它是一个数组,但是当我更新它时,我的组件不会重新渲染。这里是一个最简单的证明:

function App() {
  const [numbers, setNumbers] = React.useState([0, 1, 2, 3]);
  console.log("rendering...");
  return (
    <div className="App">
      {numbers.map(number => (
        <p>{number}</p>
      ))}
      <input
        type="text"
        value={numbers[0].toString()}
        onChange={newText => {
          let old = numbers;
          old[0] = 1;
          setNumbers(old);
        }}
      />
    </div>
  );
}
基于这段代码,似乎输入应该包含数字0以启动,并且每次更改后状态也应该随之更改。在输入框中输入“02”后,App组件没有重新渲染。但是,如果我在onChange函数中添加一个在5秒后执行的setTimeout,它会显示numbers确实已经更新。

对于组件为什么不更新有什么想法吗?

这里是证明概念的CodeSandbox

9个回答

551

你正在调用setNumbers并将其传递给已经拥有的数组。你已经改变了其中一个值,但它仍然是同一个数组,我怀疑React没有看到任何理由去重新渲染,因为状态没有改变;新数组就是旧数组。

避免这种情况的一种简单方法是将数组展开到一个新数组中:

setNumbers([...old])

5
哦,所以它内部比较的是引用而不是检查完全相等。有道理。如果我们可以决定自己重新渲染而不是让状态设置器在后台执行,那就太好了(也许已经可以做到了,我不确定)。 - Alan
1
子组件怎么样? - digz6666
1
@adamdabb 它并不是检查数组的内容,而是测试数组本身是否为同一对象。即使它确实测试了内容,你也会得到相同的结果,因为你将其与自身进行比较。 - ray
我明白这样做是可行的,因为状态更新器总是传递一个新引用,但为什么不修复导致问题的实际状态突变呢?这只是修复了症状,而真正的错误仍然存在。 - Brian Thompson
布尔值是对象中的属性吗?同样的事情可能发生。 - ray
显示剩余4条评论

57

您需要像这样复制numberslet old = [...numbers];

useState仅在值发生更改时才会更新它,因此如果它从44变为7,它将进行更新。但是,如何知道数组或对象是否已更改呢?这是通过引用来实现的,所以当您执行let old = numbers时,您只是传递了一个引用而不是创建一个新的引用。


36

您可以像这样更改状态

const [state, setState] = ({})
setState({...state})

如果您的状态是Array,您可以像这样更改

const [state, setState] = ([])
setState([...state])

5
没起作用。 - Willian
2
这对我有效!你能解释一下为什么它会这样工作吗? - wilfredonoyola
3
我可以翻译。"it works for me. but what is the reason ?" 可以翻译为“对我有效,但原因是什么?” - Siva Sankaran
1
React的状态无法理解数组或对象内部的更改。因此,您需要提供一个带有新更改的全新数组/对象,以便React能够理解它。顶部答案中已经解释了这一点。 - Arty

34

其他人已经给出了技术解决方案。对于任何困惑为什么会发生这种情况的人来说,因为setSomething()只有在先前的状态和当前状态不同的情况下才会重新渲染组件。由于JavaScript中的数组是引用类型,如果您在js中编辑数组的项,它仍然不会更改对原始数组的引用。在js的眼中,这两个数组是相同的,即使这些数组内部的原始内容是不同的。这就是为什么setSomething()无法检测对旧数组所做的更改。

请注意,如果您使用类组件并使用setState()更新状态,则无论状态是否更改,组件都将始终更新。因此,您可以将函数式组件更改为类组件作为解决方案。或者按照其他人提供的答案操作。


好的,无论是数组还是对象,因为它们都是对原始数据的引用,那么在更新状态对象属性时,为什么不总是使用Object.assign([], original)呢?为什么这不是事实上的答案并且没有在文档中提到呢? - AlxVallejo

0
我正在处理一个包含对象的数组,并尝试了上面的几个方法。
我的 useState 如下:
const [options, setOptions] = useState([
{ sno: "1", text: "" },
{ sno: "2", text: "" },
{ sno: "3", text: "" },
{ sno: "4", text: "" },
]);

现在我想在单击一个 按钮 后添加更多带有空白字段的选项,我将使用以下方法来实现我的目的:

<button
    onClick={() => {
      setOptions([...options, { sno: newArray.length + 1, text: "" }]);
    }}
  >

这解决了我的问题,我能够重新渲染组件并向数组中添加了一个对象。


-1

介绍了一个不是钩子组件的数组。例如:

const [numbers, setNumbers] = useState([0, 1, 2, 3]);

var numbersModify = []; //the value you want

最后:

setNumbers(numbersModify)

修改这个numbersModify,当钩子刷新时它将返回到0的numbersModify并且钩子将保持状态。因此,不看到更改的问题将被消除。

:D


这是不好的编程实践,它引入了不必要的变量,应使用数组展开。 - Ryan Z

-2
  //define state using useState hook
  const [numbers, setNumbers] = React.useState([0, 1, 2, 3]);

  //copy existing numbers in temp
  let tempNumbers = [...numbers];
  // modify/add no
  tempNumbers.push(4);
  tempNumbers[0] = 10;
  // set modified numbers
  setNumbers(tempNumbers);

5
请不要简单地发布代码片段,而是尝试给出更多的上下文/解释。 - kissu

-3

我对此并不感到自豪,但它能够运作。

anotherList = something
setSomething([])
setTimeout(()=>{ setSomething(anotherList)},0)

目前你的回答不够清晰,请编辑并添加更多细节,以帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Prathamesh More
这会导致不必要的状态更新。 - Ryan Z

-4

useState是React的一个钩子(hook),它为函数式组件提供了状态(State)的功能。 通常情况下,当useState变量发生变化时,它会通知React重新渲染该组件。

{
      let old = numbers;
      old[0] = 1;
      setNumbers(old);
}

在上述代码中,由于你引用的是同一个变量,它存储的是引用而不是值,因此React并不知道最新的更改,因为引用与之前相同。
为了克服这个问题,可以使用以下方法:采用深度复制(即复制值而非引用),而不是复制引用。
{
      let old = JSON.parse(JSON.stringify(numbers));
      old[0] = 1;
      setNumbers(old);
}

编程愉快 :)


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