React:useState 钩子中的 setState 在啥情况下会导致重新渲染?

Posted

技术标签:

【中文标题】React:useState 钩子中的 setState 在啥情况下会导致重新渲染?【英文标题】:React: Under what conditions does setState from the useState hook cause a re-render?React:useState 钩子中的 setState 在什么情况下会导致重新渲染? 【发布时间】:2021-12-27 23:04:48 【问题描述】:

我有以下代码 (CodeSandbox):

function App() 
  const [blah, setBlah] = useState(true);
  console.log('BLAH', blah);
  setBlah(true);

  return <button onClick=() => setBlah(true)>Blah</button>;

我了解组件顶层的setBlah(true) 导致重新渲染过多。我不明白的是,如果您注释掉*** setBlah(true) 并开始捣碎“Blah”按钮,为什么组件不会重新渲染?两者都将blah 的状态一遍又一遍地设置为true,但只有***setBlah(true) 会导致重新渲染。

来自React docs on Bailing out of a state update:“请注意,React 在退出之前可能仍需要再次渲染该特定组件。”这里的关键词是“可能”,这对我来说意味着根据文档我的情况是有效的。所以问题可能变成了,在什么情况下调用setState,使用已经设置的相同值,会导致重新渲染?知道这一点会很有用,因为人们可能希望将逻辑放在他们的 setState 方法前面,以检查值是否与当前值相同,以免导致错误的重新渲染。

【问题讨论】:

每次调用setBlah() 都会导致重新渲染。使用您原来的 sn-p,您在每次渲染时都调用 setBlah(),这会使您无限循环。另一个 setBlah() 调用是由事件触发而不是在渲染时触发,这就是它不会导致相同循环的原因。 是的,我并不是说onClicksetBlah 应该导致相同的循环——它不应该。我是说它不会导致任何重新渲染发生,甚至一次都没有。 由于您已经将 blah 的默认值设置为 true - const [blah, setBlah] = useState(true) - 捣碎按钮实际上并没有改变状态,因为它已经是 true。我认为:) 完全是这样,但是为什么这与***setBlah(true) 不同,并且在值不变时会导致无休止的重新渲染?如果*** setBlah(true) 导致重新渲染,它会无休止地重新渲染,我希望按钮单击会导致一次重新渲染。 【参考方案1】:

来自我的评论 - 因为您已经将 blah 的默认值设置为 true -

const [blah, setBlah] = useState(true);

捣碎按钮实际上并没有改变状态,因为它已经是真的了。但是,如果您执行以下操作:

return <button onClick=() => setBlah(!blah)>Blah</button>;

每次点击“Blah”按钮时,您都会在控制台中看到它在真假之间切换。

这是

setBlah(true);

在导致循环的构造函数中。它已经将 state 设置为 true,然后你告诉它在同一个构造函数中再次设置它。

【讨论】:

【参考方案2】:

通过您在问题中链接到的the documentation:

如果您将 State Hook 更新为与当前状态相同的值,React 将退出而不渲染子级或触发效果。 (React 使用 Object.is comparison algorithm。)

你可以在那里读到 React 在比较以前和新的状态值时使用Object.is。如果比较的返回值为true,那么React 不会使用setState 调用来考虑是否重新渲染。这就是为什么在 onClick 处理程序中将状态值设置为 true 不会导致重新渲染。

也就是说,在任何组件的顶层无条件调用setState 函数始终是错误的,因为它会启动协调算法(无限)。在我看来这是你问题的核心,如果你想了解 React Fiber(React 核心算法的实现),那么你可以从这里开始:https://github.com/acdlite/react-fiber-architecture

这是 React 文档中的另一条注释(需要针对功能组件进行更新):

可以立即在componentDidUpdate() 中调用setState(),但请注意,它必须像上面的示例一样包含在条件中,否则您将导致无限循环。

↳https://reactjs.org/docs/react-component.html#componentdidupdate

解释类组件生命周期方法如何转换为功能组件超出了此问题的范围(您可以在 Stack Overflow 上找到解决此问题的其他问题和答案);但是,此指令适用于您的情况。

这是一个 sn-p,显示当错误的 setState 调用被删除时,您的组件只呈现一次:

<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.16.3/babel.min.js"></script>

<div id="root"></div>

<script type="text/babel" data-type="module" data-presets="react">

const useRef, useState = React;

function Example () 
  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  const [bool, setBool] = useState(true);

  return (
    <div>
      <div>Render count: renderCountRef.current</div>
      <button onClick=() => setBool(true)>String(bool)</button>
    </div>
  );


ReactDOM.render(<Example />, document.getElementById('root'));

</script>

【讨论】:

当您在组件的顶层说setState“启动协调算法”时,这不是setStateonClick 处理程序中所做的吗?我本来预计不会发生任何和解,因为我会想象setState 在这两种情况下都会短路,因为值与传递给useState 的默认值相同,但这显然不会发生在顶部级别setState,而它似乎发生在onClicksetState 我认为您混淆了渲染和协调算法;她们不一样。渲染可以作为协调算法的结果发生,但并不总是发生。当您调用 setState 函数时(无论在哪里),它都会启动协调算法,然后再次调用您的组件函数。因此,如果您在组件的顶层调用 setState,您将在运行时创建一个无限循环。 我认为您的大多数其他问题都可以通过阅读我在回答中链接到的 React 光纤架构的描述性摘要来回答。而且,与往常一样,您可以通过查看 React 的源代码来找到最清晰的内容;这是 React v17.0.2useState 钩子的链接:github.com/facebook/react/blob/17.0.2/packages/react/src/…。 我已经用 React 文档中关于有条件地调用 setState 的附加引用更新了我的答案。 @Vampiro 如果这回答了您的问题,请随时将其标记为这样,以便其他人更容易找到它。

以上是关于React:useState 钩子中的 setState 在啥情况下会导致重新渲染?的主要内容,如果未能解决你的问题,请参考以下文章

ReactJS - 新的 useState React 钩子中的 prevState?

如何为 React 钩子(useState 等)做流类型注释?

在对数组进行排序并传递它之后,React 钩子 useState/setState 不起作用

React - useState 钩子第一次和后续设置状态时的奇怪行为

如何在 React 中正确使用 useState 钩子和 typescript?

React 用 useReducer 替换 useState,同时还使用自定义钩子