将 useState 更新为它已经保存在自定义 React 钩子内部的值会导致无限重新渲染

updating a useState to the value that it's already holding inside of a custom React hook causes infinite re-render

function useHandleURL(mode, page) {
  const [is_page_hidden, set_is_page_hidden] = useState(true);

  ...

  set_is_page_hidden(true);
}

以上将导致无限重新渲染。

我不得不这样做来解决:

function useHandleURL(mode, page) {
  const [is_page_hidden, set_is_page_hidden] = useState(true);

  ...

  if (!is_page_hidden) {
    set_is_page_hidden(true);
  }
}

这不是 React 组件内部的行为。在组件内部,如果我将 useState 设置为 true,而它已经是 true,那么它不会导致重新渲染。

有人可以确认此行为并解释为什么它会导致在 Hook 内部而不是 Component 内部无限重新渲染吗?

可以确认在函数组件主体中使用完全相同的代码呈现循环与在自定义挂钩中时相同。问题是状态 setter.

的无条件调用

useStateBailing out of a state update

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

另请注意,“React 可能仍需要在退出之前再次渲染该特定组件。”意味着 运行 再渲染一次函数,而不是“再渲染一次 DOM”,所以任何意外的 side-effects,比如将另一个状态更新加入队列都是有问题的。 整个一个函数组件的函数体的渲染函数。

尽管考虑以下代码:

function App() {
  const [is_page_hidden, set_is_page_hidden] = React.useState(true);

  const handler = () => set_is_page_hidden(true);

  React.useEffect(() => {
    console.log("RENDERED!");
  });
  
  return <button type="button" onClick={handler}>Click</button>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root" />

我们有条件地使用相同的值对状态更新进行排队,并注意到没有触发重新渲染,正如 useEffect 钩子记录每 1 个渲染周期 1 个效果所测量的那样。

结论

有条件地对状态更新进行排队是正确的。

function useHandleURL(mode, page) {
  const [is_page_hidden, set_is_page_hidden] = useState(true);

  ...

  if (!is_page_hidden) {
    set_is_page_hidden(true);
  }
}

更新

我才发现这不一定是无条件的状态更新,更多的是无意的side-effect。

  • 渲染循环

     function App() {
       const [is_page_hidden, set_is_page_hidden] = React.useState(true);
    
       set_is_page_hidden(true);
    
       return ...;
     }
    
  • 稳定,无渲染循环

        function App() {
          const [is_page_hidden, set_is_page_hidden] = React.useState(true);
    
          React.useEffect(() => {
            console.log("RENDERED!");
            set_is_page_hidden(true);
          });
      
          return "Whosebug is awesome.";
        }
    
        const rootElement = document.getElementById("root");
        ReactDOM.render(
          <React.StrictMode>
            <App />
          </React.StrictMode>,
          rootElement
        );
    
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
        <div id="root" />
    

在稳定版本中,状态更新是有意的状态更新,作为有意的 side-effect,因此不会触发重新渲染,因为状态值与之前的渲染周期相同。