React hooks 功能组件防止在状态更新时重新渲染

Posted

技术标签:

【中文标题】React hooks 功能组件防止在状态更新时重新渲染【英文标题】:React hooks function component prevent re-render on state update 【发布时间】:2019-07-28 00:37:21 【问题描述】:

我正在学习 React Hooks,这意味着我将不得不从类转向函数组件。以前在类中,我可以拥有独立于状态的类变量,我可以在不重新渲染组件的情况下更新这些变量。现在我正在尝试将一个组件重新创建为带有钩子的函数组件,我遇到了一个问题,即我不能(据我所知)为该函数创建变量,因此存储数据的唯一方法是通过useState 钩子。然而,这意味着我的组件将在状态更新时重新渲染。

我在下面的示例中进行了说明,我尝试将类组件重新创建为使用钩子的函数组件。如果有人点击它,我想为 div 设置动画,但如果用户在动画中点击,则防止动画再次被调用。

class ClassExample extends React.Component 
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => 
    if (ref) 
      this._blockRef = ref;
    
  
  
  // Animate the block.
  onClick = () => 
    if (this._isAnimating) 
      return;
    

    this._isAnimating = true;
    Velocity(this._blockRef, 
      translateX: 500,
      complete: () => 
        Velocity(this._blockRef, 
          translateX: 0,
          complete: () => 
            this._isAnimating = false;
          
        ,
        
          duration: 1000
        );
      
    ,
    
      duration: 1000
    );
  ;
  
  render() 
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick=this.onClick ref=this.onBlockRef style= width: '100px', height: '10px', backgroundColor: 'pink'></div>
      </div>
    );
  


const FunctionExample = (props) => 
  console.log("Rendering FunctionExample");
  
  const [ isAnimating, setIsAnimating ] = React.useState(false);
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => 
    if (isAnimating) 
      return;
    

    setIsAnimating(true);
    Velocity(blockRef.current, 
      translateX: 500,
      complete: () => 
        Velocity(blockRef.current, 
          translateX: 0,
          complete: () => 
            setIsAnimating(false);
          
        ,
        
          duration: 1000
        );
      
    ,
    
      duration: 1000
    );
  );
  
  return(
    <div>
      <div id='block' onClick=onClick ref=blockRef style= width: '100px', height: '10px', backgroundColor: 'red'></div>
    </div>
  );
;

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

如果您单击 ClassExample 栏(粉红色),您将看到它在制作动画时不会重新渲染,但是如果您单击 FunctionExample 栏(红色),它将在制作动画时重新渲染两次。这是因为我正在使用 setIsAnimating 导致重新渲染。我知道它可能不是很能赢得性能,但如果功能组件完全可能的话,我想阻止它。有什么建议/我做错了吗?

更新(已尝试修复,尚无解决方案): 下面的用户 lector 建议可能将 useState 的结果更改为 let 而不是 const 然后直接设置它let [isAnimating] = React.useState(false);。不幸的是,正如您在下面的 sn-p 中看到的那样,这也不起作用。单击红色条将开始其动画,单击橙色方块将使组件重新渲染,如果再次单击红色条,它将打印 isAnimating 重置为 false 即使条仍在动画中.

const FunctionExample = () => 
  console.log("Rendering FunctionExample");

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  let [isAnimating] = React.useState(false);
  
  // Var to force a re-render.
  const [ forceCount, forceUpdate ] = React.useState(0);

  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => 
    console.log("Is animating: ", isAnimating);
    if (isAnimating) 
      return;
    
    
    isAnimating = true;
    Velocity(blockRef.current, 
      translateX: 500,
      complete: () => 
        Velocity(blockRef.current, 
          translateX: 0,
          complete: () => 
            isAnimating = false;
          
        , 
          duration: 5000
        );
      
    , 
      duration: 5000
    );
  );

  return (
  <div>
    <div
      id = 'block'
      onClick = onClick
      ref = blockRef
      style = 
        
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        
      
      >
      
    </div>
    <div onClick=() => forceUpdate(forceCount + 1) 
      style = 
        
          width: '100px',
          height: '100px',
          marginTop: '12px',
          backgroundColor: 'orange'
        
      />
  </div>
  );
;

ReactDOM.render( < div > < FunctionExample / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

更新 2(解决方案): 如果您想在函数组件中有一个变量,但不让它在更新时重新渲染组件,您可以使用useRef 而不是useStateuseRef 不仅可以用于 dom 元素,而且实际上建议用于实例变量。 见:https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

【问题讨论】:

用于演示此问题的示例过于复杂和冗长。一个简单的数字输入就足够了。此外,请不要在问题中提供答案,因为它们可能会被忽略。你应该在下面添加一个答案。 随意用一个更简单的例子创建一个新问题,但这是我的用例朋友 【参考方案1】:

使用ref 在函数调用之间保留值而不触发渲染

class ClassExample extends React.Component 
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => 
    if (ref) 
      this._blockRef = ref;
    
  
  
  // Animate the block.
  onClick = () => 
    if (this._isAnimating) 
      return;
    

    this._isAnimating = true;
    Velocity(this._blockRef, 
      translateX: 500,
      complete: () => 
        Velocity(this._blockRef, 
          translateX: 0,
          complete: () => 
            this._isAnimating = false;
          
        ,
        
          duration: 1000
        );
      
    ,
    
      duration: 1000
    );
  ;
  
  render() 
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick=this.onClick ref=this.onBlockRef style= width: '100px', height: '10px', backgroundColor: 'pink'></div>
      </div>
    );
  


const FunctionExample = (props) => 
  console.log("Rendering FunctionExample");
  
  const isAnimating = React.useRef(false)
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => 
    if (isAnimating.current) 
      return;
    

    isAnimating.current = true
    
    Velocity(blockRef.current, 
      translateX: 500,
      complete: () => 
        Velocity(blockRef.current, 
          translateX: 0,
          complete: () => 
            isAnimating.current = false
          
        ,
        
          duration: 1000
        );
      
    ,
    
      duration: 1000
    );
  );
  
  return(
    <div>
      <div id='block' onClick=onClick ref=blockRef style= width: '100px', height: '10px', backgroundColor: 'red'></div>
    </div>
  );
;

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

【讨论】:

好了,谢谢老兄。我还没有玩过 useRef.. 这正是医生所要求的。 谢谢,我什至没想过要为此查看useRef,但显然他们确实建议这样做:reactjs.org/docs/…【参考方案2】:

为什么你认为你不能像使用类一样使用内部变量?

好吧,感觉有点脏,但是改变 useState 状态怎么样? 8)

不,这不符合预期 重新渲染时重置状态

所以由于这与组件的实际渲染无关,可能我们的逻辑需要基于动画本身。这个特殊的问题可以通过在元素被动画化时检查速度在元素上设置的类来解决。

const FunctionExample = ( count ) => 
  console.log("Rendering FunctionExample", count);

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  // let [isAnimating] = React.useState(false);
  
  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => 
    // use feature of the anim itself
    if (/velocity-animating/.test(blockRef.current.className)) 
      return;
    
    console.log("animation triggered");
    
    Velocity(blockRef.current, 
      translateX: 500,
      complete: () => 
        Velocity(blockRef.current, 
          translateX: 0,
        , 
          duration: 1000
        );
      
    , 
      duration: 5000
    );
  );

  return (
  <div>
    <div
      id = 'block'
      onClick = onClick
      ref = blockRef
      style = 
        
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        
      
      >
      
    </div>
  </div>
  );
;

const Counter = () => 
  const [count, setCount] = React.useState(0);
  return <div>
    <FunctionExample count=count />
    <button onClick=() => setCount(c => c + 1)>Count</button>
  </div>;


ReactDOM.render( < div > < Counter / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

【讨论】:

那些将在每次组件渲染时重新分配(因为函数组件实际上是渲染函数),因此它将覆盖它以前的值。我承认在这个例子中没有什么区别,但对于有额外原因的组件可能会重新渲染,这将是一个问题。说某事会导致块在动画中间重新渲染,如果用户单击它,那么它将排队另一个动画,因为 isAnimating 将再次设置为 false,即使它仍然是动画中间。 好的,我明白了。您可以将 isAnimating 的声明移到组件函数之外,但我猜它会应用于组件的所有渲染实例。 将其修改为改变 useState 状态.. 不推荐使用,只是因为组件不会重新渲染 (afaik),但这就是您在这种情况下想要的。 不,我现在没有想法。不过有趣的问题。我认为这可能只是你不能做的事情。理想情况下,组件只是它的 props 和 state 的渲染,这是试图解决这个问题。 这是一次不错的尝试。理想情况下,组件将是它的道具和状态的渲染,这就是为什么这个isAnimated var 在类示例中既不是状态也不是道具,因为它对渲染本身没有影响。功能组件似乎没有此功能,这使我无法存储状态以外的变量,这会导致不幸的重新渲染。

以上是关于React hooks 功能组件防止在状态更新时重新渲染的主要内容,如果未能解决你的问题,请参考以下文章

react hook介绍

react hook介绍

react---Hooks的基本使用---巷子

React拓展 - setState - 路由组件懒加载 - Hooks - Fragment - Context - PureComponent - 插槽 - 错误边界 - 组件通信方式总结(代码片

在 React Hooks 中使用 const

反应钩子。无法对未安装的组件执行 React 状态更新