反应挂钩函数组件可防止在状态更新时重新呈现

时间:2019-03-06 11:03:16

标签: reactjs react-hooks

我正在学习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导致重新渲染。我知道这可能不是很能赢得性能,但是如果功能组件完全有可能的话,我想阻止它。有任何建议/我做错了什么吗?

更新(尝试修复,尚无解决方案): 下面的用户lecstor建议可能将useState的结果更改为let而不是const,然后直接将其设置为let [isAnimating] = React.useState(false);。不幸的是,这也无法正常工作,如下面的代码片段所示。单击红色条将开始其动画,单击橙色方块将重新渲染组件,如果再次单击红色条,它将打印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

2 个答案:

答案 0 :(得分:2)

使用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>

答案 1 :(得分:1)

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

好的,感觉有点脏,但是如何改变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>