【问题标题】:React useState setters causing re-render反应 useState 设置器导致重新渲染
【发布时间】:2020-02-25 01:42:57
【问题描述】:

我想知道在 const [state, setState] = useState(0) 中的设置器是否会触发组件重新渲染。因此,如果我将设置器作为道具传递给组件,它是否应该触发重新渲染,如果是,为什么?有什么办法可以避免吗?

我创建了一个非常简单的沙箱来演示该行为:https://codesandbox.io/s/bold-maxwell-qj5mi

在查看console.logs 时,我们可以看到每次单击按钮组件都会重新呈现,而传递给它的incrementCounter 函数没有改变。什么给了?

【问题讨论】:

  • 如果您不想触发重新渲染,请根据情况使用useRef或useMemo

标签: reactjs react-hooks


【解决方案1】:

如果您memoize 按钮,您不会遇到这种行为。

具体来说,这个:

const Button = memo(({ incrementCounter }) => {
  const renderCount = useRef(1);
  console.log("button rendered: ", renderCount.current);
  renderCount.current++;
  return <button onClick={incrementCounter}>Increment</button>;
});

CodeSandbox 镜像:


更新

如果你想要something from the docs,第一句话会告诉你为什么会这样。我知道那是setState,但同样的概念遵循useState 钩子,但是那种糟糕的文档。 You can check out 文档的这一部分,特别是“第 9 行:”...

请记住,当 X 组件的状态发生变化时,X 组件及其所有子组件都会重新渲染。我知道 React 会告诉你“提升状态”,这是我从来没有理解过的,因为提升状态会导致大量的重新渲染。

这就是按钮重新渲染的原因......因为其父级中的状态正在发生变化。父级 (&lt;App /&gt;) 的 counter 状态已更改,这会触发 &lt;App /&gt; 组件及其子级(包括 &lt;Button /&gt;)的重新渲染。

在我看来,就状态和重新渲染而言,React 很难控制,Redux 可以提供帮助,但总的来说,像 memouseCallback 等对我来说都是创可贴。如果你把你的状态放在错误的组件中,你将会有一段糟糕的时光。

包装&lt;Button /&gt; 组件in memo 基本上是说:如果这个组件有一个父级(在我们的例子中是&lt;App /&gt;),并且那个父级重新渲染,我想看看我们所有的道具,如果我们的道具与上次收到的没有变化,不要重新渲染。本质上,只有在我们的道具发生变化时才重新渲染。这就是memo 修复这个问题的原因。因为我们用来处理incrementCounter 属性的函数没有改变——它保持不变。

我在下面添加了一些示例来证明这一点。


原始答案/片段:

const { memo, useState, useCallback, useEffect, useRef } = React;
const { render } = ReactDOM;

const App = () => {
  const [counter, setCounter] = useState(0);
  const incrementCounter = useCallback(() => {
    setCounter(c => c + 1);
  }, [setCounter]);

  useEffect(() => {
    console.log("increment changed!");
  }, [incrementCounter]);

  return (
    <div>
      <CountValue counter={counter} />
      <Button incrementCounter={incrementCounter} />
    </div>
  );
}

const CountValue = ({ counter }) => {
  return <div>Count value: {counter}</div>;
};

const Button = memo(({ incrementCounter }) => {
  const renderCount = useRef(1);
  console.log("button rendered: ", renderCount.current);
  renderCount.current++;
  return <button onClick={incrementCounter}>Increment</button>
});

render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

片段 #2:

这个 sn-p 显示了如何重新渲染所有内容,而不仅仅是按钮。

const { useState, useEffect } = React;
const { render } = ReactDOM;

const App = () => {
  console.log("App rendered");
  const [counter, setCounter] = useState(0);
  const incrementCounter = () => setCounter(c => c + 1);

  useEffect(() => {
    console.log(" - Increment fired!");
    console.log();
  }, [incrementCounter]);

  return (
    <div>
      <CountValue counter={counter} />
      <Button incrementCounter={incrementCounter} />
      <p>Open console</p>
    </div>
  );
}

const CountValue = ({ counter }) => {
  console.log("CountValue rendered");
  return <div>Count value: {counter}</div>;
};

const Button = ({ incrementCounter }) => {
  console.log("Button rendered");
  return <button onClick={incrementCounter}>Increment</button>
};

render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

片段 #3:

这个 sn-p 显示如果您将状态等移动到 &lt;CountValue /&gt; 组件中,&lt;App /&gt; 组件不会重新渲染..

const { useState, useEffect } = React;
const { render } = ReactDOM;

const App = () => {
  console.log("App rendered");
  
  return (
    <div>
      <CountValue />
      <p>Open console</p>
    </div>
  );
}

const CountValue = () => {
  console.log("CountValue rendered");
  
  const [counter, setCounter] = useState(0);
  const incrementCounter = () => setCounter(c => c + 1);
  
  return (
    <div>
      <div>Count value: {counter}</div>
      <Button incrementCounter={incrementCounter} />
    </div>
  );
};

const Button = ({ incrementCounter }) => {
  console.log("Button rendered");
  console.log();
  return <button onClick={incrementCounter}>Increment</button>
};

render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

片段 #4:

这个 sn-p 更像是一个思想实验,展示了如何使用渲染道具。

const { useState, useEffect } = React;
const { render } = ReactDOM;

const App = () => {
  console.log("App rendered");

  return (
    <div>
      <CountValue present={({ increment, counter }) => {
        return (
          <div><Button incrementCounter={() => increment()} />
          <p>Counter Value: {counter}</p></div>
        )
      }} />
      <p>Open console</p>
    </div>
  );
}

const CountValue = ({ present }) => {
  const [counter, setCounter] = useState(0);
  
  const increment = () => {
    setCounter(c => c + 1);
  }
  
  console.log("CountValue rendered");
  return (
    <React.Fragment>
      {present({ increment, counter })}
    </React.Fragment>
  );
};

const Button = ({ incrementCounter }) => {
  console.log("Button rendered");
  return <button onClick={incrementCounter}>Increment</button>
};

render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

片段 #5:

看起来这就是你所追求的.. 这只会重新渲染 CountValue.. 这是通过将setCounter 方法(由useState 生成)传递给父级来完成的,通过回调对象。

这样父级可以操纵状态,而不必实际持有状态。

const { useState, useEffect } = React;
const { render } = ReactDOM;

const App = () => {
  console.log("App rendered");
  let increaseCount;

  return (
    <div>
      <CountValue callback={({increment}) => increaseCount = increment} />
      <Button incrementCounter={() => increaseCount()} />
      <p>Open console</p>
    </div>
  );
}

const CountValue = ({ callback }) => {
  console.log("CountValue rendered");
  const [counter, setCounter] = useState(0);
  
  callback && callback({
    increment: () => setCounter(c => c + 1)
  });
  
  return <p>Counter Value: {counter}</p>;
};

const Button = ({ incrementCounter }) => {
  console.log("Button rendered");
  return <button onClick={incrementCounter}>Increment</button>
};

render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

【讨论】:

  • 这太棒了!我会将此标记为答案,但我仍然很好奇为什么设置器首先会触发重新渲染。知道这是为什么吗?或者我可以在文档中的哪里阅读到它?
  • 我已经用解释和一些演示更新了我的答案。希望对您有所帮助!
  • @CaptainStiggz 查看我的最新更新,特别是 SNIPPET #5,因为它展示了如何仅使用 CountValue 组件重新完成此操作-渲染。
【解决方案2】:

是的,这是预期的。

如果您避免重新渲染,您将看不到屏幕中设置的值。

如果您想创建一个可以在不触发重新渲染的情况下更改的值,请使用useRef

【讨论】:

  • 嗯,我希望 counter 的值会触发 Counter 组件上的重新渲染,但 setCounter 触发 Button 组件上的重新渲染是出乎意料的。价值正在改变,但我不认为 setter 是。换句话说,我希望 Counter 组件会重新渲染,而 Button 组件不会。
  • 如果你看向沙盒的顶部,我有一个自定义钩子useStaticState,它被注释掉了。这将 setter 包装在 useRef 中,但仍会触发 Button 组件上的重新渲染,这让我感到非常惊讶。
  • @CaptainStiggz 当 React 组件重新渲染时,所有子组件默认重新渲染
猜你喜欢
  • 2022-01-04
  • 2022-01-22
  • 1970-01-01
  • 2020-07-18
  • 1970-01-01
  • 1970-01-01
  • 2022-06-15
  • 1970-01-01
相关资源
最近更新 更多